diff --git a/development/__pycache__/puepy_hooks.cpython-312.pyc b/development/__pycache__/puepy_hooks.cpython-312.pyc index 4cddbd8..9e4b07e 100644 Binary files a/development/__pycache__/puepy_hooks.cpython-312.pyc and b/development/__pycache__/puepy_hooks.cpython-312.pyc differ diff --git a/development/guide/pyscript-config/index.html b/development/guide/pyscript-config/index.html index b3e5e69..16d8d63 100644 --- a/development/guide/pyscript-config/index.html +++ b/development/guide/pyscript-config/index.html @@ -1256,7 +1256,7 @@

PyScript Config

], "js_modules": { "main": { - "https://cdn.jsdelivr.net/npm/morphdom@2.7.2/+esm": "morphdom" + "https://cdn.jsdelivr.net/npm/morphdom@2.7.4/+esm": "morphdom" } } } diff --git a/development/objects.inv b/development/objects.inv index 23fae55..6ed71e6 100644 Binary files a/development/objects.inv and b/development/objects.inv differ diff --git a/development/reference/application/index.html b/development/reference/application/index.html index 0f93374..8a482eb 100644 --- a/development/reference/application/index.html +++ b/development/reference/application/index.html @@ -1651,12 +1651,7 @@

puepy.Application

Source code in puepy/application.py -
 85
- 86
- 87
- 88
- 89
- 90
+
 90
  91
  92
  93
@@ -1944,300 +1939,305 @@ 

puepy.Application

375 376 377 -378
class Application(Stateful):
-    """
-    The main application class for PuePy. It manages the state, storage, router, and pages for the application.
-
-    Attributes:
-        state (ReactiveDict): The state object for the application.
-        session_storage (BrowserStorage): The session storage object for the application.
-        local_storage (BrowserStorage): The local storage object for the application.
-        router (Router): The router object for the application, if any
-        default_page (Page): The default page to mount if no route is matched.
-        active_page (Page): The currently active page.
-        not_found_page (Page): The page to mount when a 404 error occurs.
-        forbidden_page (Page): The page to mount when a 403 error occurs.
-        unauthorized_page (Page): The page to mount when a 401 error occurs.
-        error_page (Page): The page to mount when an error occurs.
-    """
-
-    def __init__(self, element_id_generator=None):
-        self.state = ReactiveDict(self.initial())
-        self.add_context("state", self.state)
-
-        if is_server_side:
-            self.session_storage = None
-            self.local_storage = None
-        else:
-            from js import localStorage, sessionStorage
-
-            self.session_storage = BrowserStorage(sessionStorage, "session_storage")
-            self.local_storage = BrowserStorage(localStorage, "local_storage")
-        self.router = None
-        self._selector_or_element = None
-        self.default_page = None
-        self.active_page = None
-
-        self.not_found_page = GenericErrorPage
-        self.forbidden_page = GenericErrorPage
-        self.unauthorized_page = GenericErrorPage
-        self.error_page = TracebackErrorPage
+378
+379
+380
+381
+382
+383
class Application(Stateful):
+    """
+    The main application class for PuePy. It manages the state, storage, router, and pages for the application.
+
+    Attributes:
+        state (ReactiveDict): The state object for the application.
+        session_storage (BrowserStorage): The session storage object for the application.
+        local_storage (BrowserStorage): The local storage object for the application.
+        router (Router): The router object for the application, if any
+        default_page (Page): The default page to mount if no route is matched.
+        active_page (Page): The currently active page.
+        not_found_page (Page): The page to mount when a 404 error occurs.
+        forbidden_page (Page): The page to mount when a 403 error occurs.
+        unauthorized_page (Page): The page to mount when a 401 error occurs.
+        error_page (Page): The page to mount when an error occurs.
+    """
+
+    def __init__(self, element_id_generator=None):
+        self.state = ReactiveDict(self.initial())
+        self.add_context("state", self.state)
+
+        if is_server_side:
+            self.session_storage = None
+            self.local_storage = None
+        else:
+            from js import localStorage, sessionStorage
+
+            self.session_storage = BrowserStorage(sessionStorage, "session_storage")
+            self.local_storage = BrowserStorage(localStorage, "local_storage")
+        self.router = None
+        self._selector_or_element = None
+        self.default_page = None
+        self.active_page = None
 
-        self.element_id_generator = element_id_generator or DefaultIdGenerator()
-
-    def install_router(self, router_class, **kwargs):
-        """
-        Install a router in the application.
-
-        Args:
-            router_class (class): A class that implements the router logic for the application. At this time, only
-                `puepy.router.Router` is available.
-            **kwargs: Additional keyword arguments that can be passed to the router_class constructor.
-        """
-        self.router = router_class(application=self, **kwargs)
-        if not is_server_side:
-            add_event_listener(window, "popstate", self._on_popstate)
-
-    def page(self, route=None, name=None):
-        """
-        A decorator for `Page` classes which adds the page to the application with a specified route and name.
-
-        Intended to be called as a decorator.
-
-        Args:
-            route (str): The route for the page. Default is None.
-            name (str): The name of the page. If left None, page class is used as the name.
-
-        Examples:
-            ``` py
-            app = Application()
-            @app.page("/my-page")
-            class MyPage(Page):
-                ...
-            ```
-        """
-        if route:
-            if not self.router:
-                raise Exception("Router not installed")
-
-            def decorator(func):
-                self.router.add_route(route, func, name=name)
-                return func
-
-            return decorator
-        else:
-
-            def decorator(func):
-                self.default_page = func
-                return func
-
-            return decorator
-
-    def _on_popstate(self, event):
-        if self.router.link_mode == self.router.LINK_MODE_HASH:
-            self.mount(self._selector_or_element, window.location.hash.split("#", 1)[-1])
-        elif self.router.link_mode in (self.router.LINK_MODE_DIRECT, self.router.LINK_MODE_HTML5):
-            self.mount(self._selector_or_element, window.location.pathname)
-
-    def remount(self, path=None, page_kwargs=None):
-        """
-        Remounts the selected element or selector with the specified path and page_kwargs.
-
-        Args:
-            path (str): The new path to be used for remounting the element or selector. Default is None.
-            page_kwargs (dict): Additional page kwargs to be passed when remounting. Default is None.
-
-        """
-        self.mount(self._selector_or_element, path=path, page_kwargs=page_kwargs)
-
-    def mount(self, selector_or_element, path=None, page_kwargs=None):
-        """
-        Mounts a page onto the specified selector or element with optional path and page_kwargs.
-
-        Args:
-            selector_or_element: The selector or element on which to mount the page.
-            path: Optional path to match against the router. Defaults to None.
-            page_kwargs: Optional keyword arguments to pass to the mounted page. Defaults to None.
+        self.not_found_page = GenericErrorPage
+        self.forbidden_page = GenericErrorPage
+        self.unauthorized_page = GenericErrorPage
+        self.error_page = TracebackErrorPage
+
+        self.element_id_generator = element_id_generator or DefaultIdGenerator()
+
+    def install_router(self, router_class, **kwargs):
+        """
+        Install a router in the application.
+
+        Args:
+            router_class (class): A class that implements the router logic for the application. At this time, only
+                `puepy.router.Router` is available.
+            **kwargs: Additional keyword arguments that can be passed to the router_class constructor.
+        """
+        self.router = router_class(application=self, **kwargs)
+        if not is_server_side:
+            add_event_listener(window, "popstate", self._on_popstate)
+
+    def page(self, route=None, name=None):
+        """
+        A decorator for `Page` classes which adds the page to the application with a specified route and name.
+
+        Intended to be called as a decorator.
+
+        Args:
+            route (str): The route for the page. Default is None.
+            name (str): The name of the page. If left None, page class is used as the name.
+
+        Examples:
+            ``` py
+            app = Application()
+            @app.page("/my-page")
+            class MyPage(Page):
+                ...
+            ```
+        """
+        if route:
+            if not self.router:
+                raise Exception("Router not installed")
+
+            def decorator(func):
+                self.router.add_route(route, func, name=name)
+                return func
+
+            return decorator
+        else:
+
+            def decorator(func):
+                self.default_page = func
+                return func
+
+            return decorator
+
+    def _on_popstate(self, event):
+        if self.router.link_mode == self.router.LINK_MODE_HASH:
+            self.mount(self._selector_or_element, window.location.hash.split("#", 1)[-1])
+        elif self.router.link_mode in (self.router.LINK_MODE_DIRECT, self.router.LINK_MODE_HTML5):
+            self.mount(self._selector_or_element, window.location.pathname)
+
+    def remount(self, path=None, page_kwargs=None):
+        """
+        Remounts the selected element or selector with the specified path and page_kwargs.
+
+        Args:
+            path (str): The new path to be used for remounting the element or selector. Default is None.
+            page_kwargs (dict): Additional page kwargs to be passed when remounting. Default is None.
+
+        """
+        self.mount(self._selector_or_element, path=path, page_kwargs=page_kwargs)
+
+    def mount(self, selector_or_element, path=None, page_kwargs=None):
+        """
+        Mounts a page onto the specified selector or element with optional path and page_kwargs.
 
-        Returns:
-            (Page): The mounted page instance
-        """
-        if page_kwargs is None:
-            page_kwargs = {}
-
-        self._selector_or_element = selector_or_element
-
-        if self.router:
-            path = path or self.current_path
-            route, arguments = self.router.match(path)
-            if arguments:
-                page_kwargs.update(arguments)
-
-            if route:
-                page_class = route.page
-            elif path in ("", "/") and self.default_page:
-                page_class = self.default_page
-            elif self.not_found_page:
-                page_class = self.not_found_page
-            else:
-                return None
-        elif self.default_page:
-            route = None
-            page_class = self.default_page
-        else:
-            return None
-
-        self.active_page = None
-        try:
-            self.mount_page(
-                selector_or_element=selector_or_element,
-                page_class=page_class,
-                route=route,
-                page_kwargs=page_kwargs,
-                handle_exceptions=True,
-            )
-        except Exception as e:
-            self.handle_error(e)
-        return self.active_page
-
-    @property
-    def current_path(self):
-        """
-        Returns the current path based on the router's link mode.
+        Args:
+            selector_or_element: The selector or element on which to mount the page.
+            path: Optional path to match against the router. Defaults to None.
+            page_kwargs: Optional keyword arguments to pass to the mounted page. Defaults to None.
+
+        Returns:
+            (Page): The mounted page instance
+        """
+        if page_kwargs is None:
+            page_kwargs = {}
+
+        self._selector_or_element = selector_or_element
+
+        if self.router:
+            path = path or self.current_path
+            route, arguments = self.router.match(path)
+            if arguments:
+                page_kwargs.update(arguments)
+
+            if route:
+                page_class = route.page
+            elif path in ("", "/") and self.default_page:
+                page_class = self.default_page
+            elif self.not_found_page:
+                page_class = self.not_found_page
+            else:
+                return None
+        elif self.default_page:
+            route = None
+            page_class = self.default_page
+        else:
+            return None
+
+        self.active_page = None
+        try:
+            self.mount_page(
+                selector_or_element=selector_or_element,
+                page_class=page_class,
+                route=route,
+                page_kwargs=page_kwargs,
+                handle_exceptions=True,
+            )
+        except Exception as e:
+            self.handle_error(e)
+        return self.active_page
 
-        Returns:
-            str: The current path.
-        """
-        if self.router.link_mode == self.router.LINK_MODE_HASH:
-            return window.location.hash.split("#", 1)[-1]
-        elif self.router.link_mode in (self.router.LINK_MODE_DIRECT, self.router.LINK_MODE_HTML5):
-            return window.location.pathname
-        else:
-            return ""
-
-    def mount_page(self, selector_or_element, page_class, route, page_kwargs, handle_exceptions=True):
-        """
-        Mounts a page on the specified selector or element with the given parameters.
-
-        Args:
-            selector_or_element (str or Element): The selector string or element to mount the page on.
-            page_class (class): The page class to mount.
-            route (str): The route for the page.
-            page_kwargs (dict): Additional keyword arguments to pass to the page class.
-            handle_exceptions (bool, optional): Determines whether to handle exceptions thrown during mounting.
-                Defaults to True.
-        """
-        page_class._expanded_props()
-
-        # For security, we only pass props to the page that are defined in the page's props
-        #
-        # We also handle the list or not-list props for multiple or single values
-        # (eg, ?foo=1&foo=2 -> ["1", "2"] if needed)
-        #
-        prop_args = {}
-        prop: Prop
-        for prop in page_class.props_expanded.values():
-            if prop.name in page_kwargs:
-                value = page_kwargs.pop(prop.name)
-                if prop.type is list:
-                    prop_args[prop.name] = value if isinstance(value, list) else [value]
-                else:
-                    prop_args[prop.name] = value if not isinstance(value, list) else value[0]
-
-        self.active_page: Page = page_class(matched_route=route, application=self, extra_args=page_kwargs, **prop_args)
-        try:
-            self.active_page.mount(selector_or_element)
-        except exceptions.PageError as e:
-            if handle_exceptions:
-                self.handle_page_error(e)
-            else:
-                raise
-
-    def handle_page_error(self, exc):
-        """
-        Handles page error based on the given exception by inspecting the exception type and passing it along to one
-        of:
+    @property
+    def current_path(self):
+        """
+        Returns the current path based on the router's link mode.
+
+        Returns:
+            str: The current path.
+        """
+        if self.router.link_mode == self.router.LINK_MODE_HASH:
+            return window.location.hash.split("#", 1)[-1]
+        elif self.router.link_mode in (self.router.LINK_MODE_DIRECT, self.router.LINK_MODE_HTML5):
+            return window.location.pathname
+        else:
+            return ""
+
+    def mount_page(self, selector_or_element, page_class, route, page_kwargs, handle_exceptions=True):
+        """
+        Mounts a page on the specified selector or element with the given parameters.
+
+        Args:
+            selector_or_element (str or Element): The selector string or element to mount the page on.
+            page_class (class): The page class to mount.
+            route (str): The route for the page.
+            page_kwargs (dict): Additional keyword arguments to pass to the page class.
+            handle_exceptions (bool, optional): Determines whether to handle exceptions thrown during mounting.
+                Defaults to True.
+        """
+        page_class._expanded_props()
+
+        # For security, we only pass props to the page that are defined in the page's props
+        #
+        # We also handle the list or not-list props for multiple or single values
+        # (eg, ?foo=1&foo=2 -> ["1", "2"] if needed)
+        #
+        prop_args = {}
+        prop: Prop
+        for prop in page_class.props_expanded.values():
+            if prop.name in page_kwargs:
+                value = page_kwargs.pop(prop.name)
+                if prop.type is list:
+                    prop_args[prop.name] = value if isinstance(value, list) else [value]
+                else:
+                    prop_args[prop.name] = value if not isinstance(value, list) else value[0]
+
+        self.active_page: Page = page_class(matched_route=route, application=self, extra_args=page_kwargs, **prop_args)
+        try:
+            self.active_page.mount(selector_or_element)
+        except exceptions.PageError as e:
+            if handle_exceptions:
+                self.handle_page_error(e)
+            else:
+                raise
 
-        - `handle_not_found`
-        - `handle_forbidden`
-        - `handle_unauthorized`
-        - `handle_redirect`
-        - `handle_error`
-
-        Args:
-            exc (Exception): The exception object representing the page error.
-        """
-        if isinstance(exc, exceptions.NotFound):
-            self.handle_not_found(exc)
-        elif isinstance(exc, exceptions.Forbidden):
-            self.handle_forbidden(exc)
-        elif isinstance(exc, exceptions.Unauthorized):
-            self.handle_unauthorized(exc)
-        elif isinstance(exc, exceptions.Redirect):
-            self.handle_redirect(exc)
-        else:
-            self.handle_error(exc)
-
-    def handle_not_found(self, exception):
-        """
-        Handles the exception for not found page. By default, it mounts the self.not_found_page class and passes it
-        the exception as an argument.
+    def handle_page_error(self, exc):
+        """
+        Handles page error based on the given exception by inspecting the exception type and passing it along to one
+        of:
+
+        - `handle_not_found`
+        - `handle_forbidden`
+        - `handle_unauthorized`
+        - `handle_redirect`
+        - `handle_error`
+
+        Args:
+            exc (Exception): The exception object representing the page error.
+        """
+        if isinstance(exc, exceptions.NotFound):
+            self.handle_not_found(exc)
+        elif isinstance(exc, exceptions.Forbidden):
+            self.handle_forbidden(exc)
+        elif isinstance(exc, exceptions.Unauthorized):
+            self.handle_unauthorized(exc)
+        elif isinstance(exc, exceptions.Redirect):
+            self.handle_redirect(exc)
+        else:
+            self.handle_error(exc)
 
-        Args:
-            exception (Exception): The exception that occurred.
-        """
-        self.mount_page(
-            self._selector_or_element, self.not_found_page, None, {"error": exception}, handle_exceptions=False
-        )
-
-    def handle_forbidden(self, exception):
-        """
-        Handles the exception for forbidden page. By default, it mounts the self.forbidden_page class and passes it
-        the exception as an argument.
+    def handle_not_found(self, exception):
+        """
+        Handles the exception for not found page. By default, it mounts the self.not_found_page class and passes it
+        the exception as an argument.
+
+        Args:
+            exception (Exception): The exception that occurred.
+        """
+        self.mount_page(
+            self._selector_or_element, self.not_found_page, None, {"error": exception}, handle_exceptions=False
+        )
 
-        Args:
-            exception (Exception): The exception that occurred.
-        """
-        self.mount_page(
-            self._selector_or_element,
-            self.forbidden_page,
-            None,
-            {"error": exception},
-            handle_exceptions=False,
-        )
-
-    def handle_unauthorized(self, exception):
-        """
-        Handles the exception for unauthorized page. By default, it mounts the self.unauthorized_page class and passes it
-        the exception as an argument.
+    def handle_forbidden(self, exception):
+        """
+        Handles the exception for forbidden page. By default, it mounts the self.forbidden_page class and passes it
+        the exception as an argument.
+
+        Args:
+            exception (Exception): The exception that occurred.
+        """
+        self.mount_page(
+            self._selector_or_element,
+            self.forbidden_page,
+            None,
+            {"error": exception},
+            handle_exceptions=False,
+        )
 
-        Args:
-            exception (Exception): The exception that occurred.
-        """
-        self.mount_page(
-            self._selector_or_element, self.unauthorized_page, None, {"error": exception}, handle_exceptions=False
-        )
-
-    def handle_error(self, exception):
-        """
-        Handles the exception for application or unknown errors. By default, it mounts the self.error_page class and
-        passes it the exception as an argument.
+    def handle_unauthorized(self, exception):
+        """
+        Handles the exception for unauthorized page. By default, it mounts the self.unauthorized_page class and passes it
+        the exception as an argument.
+
+        Args:
+            exception (Exception): The exception that occurred.
+        """
+        self.mount_page(
+            self._selector_or_element, self.unauthorized_page, None, {"error": exception}, handle_exceptions=False
+        )
 
-        Args:
-            exception (Exception): The exception that occurred.
-        """
-        self.mount_page(self._selector_or_element, self.error_page, None, {"error": exception}, handle_exceptions=False)
-        if is_server_side:
-            raise
-
-    def handle_redirect(self, exception):
-        """
-        Handles a redirect exception by navigating to the given path.
-
-        Args:
-            exception (RedirectException): The redirect exception containing the path to navigate to.
-        """
-        self.router.navigate_to_path(exception.path)
+    def handle_error(self, exception):
+        """
+        Handles the exception for application or unknown errors. By default, it mounts the self.error_page class and
+        passes it the exception as an argument.
+
+        Args:
+            exception (Exception): The exception that occurred.
+        """
+        self.mount_page(self._selector_or_element, self.error_page, None, {"error": exception}, handle_exceptions=False)
+        if is_server_side:
+            raise
+
+    def handle_redirect(self, exception):
+        """
+        Handles a redirect exception by navigating to the given path.
+
+        Args:
+            exception (RedirectException): The redirect exception containing the path to navigate to.
+        """
+        self.router.navigate_to_path(exception.path)
 
@@ -2310,27 +2310,27 @@

Source code in puepy/application.py -
359
-360
-361
-362
-363
-364
+
def handle_error(self, exception):
-    """
-    Handles the exception for application or unknown errors. By default, it mounts the self.error_page class and
-    passes it the exception as an argument.
-
-    Args:
-        exception (Exception): The exception that occurred.
-    """
-    self.mount_page(self._selector_or_element, self.error_page, None, {"error": exception}, handle_exceptions=False)
-    if is_server_side:
-        raise
+369
+370
+371
+372
+373
+374
def handle_error(self, exception):
+    """
+    Handles the exception for application or unknown errors. By default, it mounts the self.error_page class and
+    passes it the exception as an argument.
+
+    Args:
+        exception (Exception): The exception that occurred.
+    """
+    self.mount_page(self._selector_or_element, self.error_page, None, {"error": exception}, handle_exceptions=False)
+    if is_server_side:
+        raise
 
@@ -2373,12 +2373,7 @@

Source code in puepy/application.py -
331
-332
-333
-334
-335
-336
+
336
 337
 338
 339
@@ -2387,21 +2382,26 @@ 

342 343 344 -345

def handle_forbidden(self, exception):
-    """
-    Handles the exception for forbidden page. By default, it mounts the self.forbidden_page class and passes it
-    the exception as an argument.
-
-    Args:
-        exception (Exception): The exception that occurred.
-    """
-    self.mount_page(
-        self._selector_or_element,
-        self.forbidden_page,
-        None,
-        {"error": exception},
-        handle_exceptions=False,
-    )
+345
+346
+347
+348
+349
+350
def handle_forbidden(self, exception):
+    """
+    Handles the exception for forbidden page. By default, it mounts the self.forbidden_page class and passes it
+    the exception as an argument.
+
+    Args:
+        exception (Exception): The exception that occurred.
+    """
+    self.mount_page(
+        self._selector_or_element,
+        self.forbidden_page,
+        None,
+        {"error": exception},
+        handle_exceptions=False,
+    )
 
@@ -2444,27 +2444,27 @@

Source code in puepy/application.py -
319
-320
-321
-322
-323
-324
+
def handle_not_found(self, exception):
-    """
-    Handles the exception for not found page. By default, it mounts the self.not_found_page class and passes it
-    the exception as an argument.
-
-    Args:
-        exception (Exception): The exception that occurred.
-    """
-    self.mount_page(
-        self._selector_or_element, self.not_found_page, None, {"error": exception}, handle_exceptions=False
-    )
+329
+330
+331
+332
+333
+334
def handle_not_found(self, exception):
+    """
+    Handles the exception for not found page. By default, it mounts the self.not_found_page class and passes it
+    the exception as an argument.
+
+    Args:
+        exception (Exception): The exception that occurred.
+    """
+    self.mount_page(
+        self._selector_or_element, self.not_found_page, None, {"error": exception}, handle_exceptions=False
+    )
 
@@ -2514,12 +2514,7 @@

Source code in puepy/application.py -
294
-295
-296
-297
-298
-299
+
299
 300
 301
 302
@@ -2537,30 +2532,35 @@ 

314 315 316 -317

def handle_page_error(self, exc):
-    """
-    Handles page error based on the given exception by inspecting the exception type and passing it along to one
-    of:
-
-    - `handle_not_found`
-    - `handle_forbidden`
-    - `handle_unauthorized`
-    - `handle_redirect`
-    - `handle_error`
-
-    Args:
-        exc (Exception): The exception object representing the page error.
-    """
-    if isinstance(exc, exceptions.NotFound):
-        self.handle_not_found(exc)
-    elif isinstance(exc, exceptions.Forbidden):
-        self.handle_forbidden(exc)
-    elif isinstance(exc, exceptions.Unauthorized):
-        self.handle_unauthorized(exc)
-    elif isinstance(exc, exceptions.Redirect):
-        self.handle_redirect(exc)
-    else:
-        self.handle_error(exc)
+317
+318
+319
+320
+321
+322
def handle_page_error(self, exc):
+    """
+    Handles page error based on the given exception by inspecting the exception type and passing it along to one
+    of:
+
+    - `handle_not_found`
+    - `handle_forbidden`
+    - `handle_unauthorized`
+    - `handle_redirect`
+    - `handle_error`
+
+    Args:
+        exc (Exception): The exception object representing the page error.
+    """
+    if isinstance(exc, exceptions.NotFound):
+        self.handle_not_found(exc)
+    elif isinstance(exc, exceptions.Forbidden):
+        self.handle_forbidden(exc)
+    elif isinstance(exc, exceptions.Unauthorized):
+        self.handle_unauthorized(exc)
+    elif isinstance(exc, exceptions.Redirect):
+        self.handle_redirect(exc)
+    else:
+        self.handle_error(exc)
 
@@ -2602,21 +2602,21 @@

Source code in puepy/application.py -
371
-372
-373
-374
-375
-376
+
def handle_redirect(self, exception):
-    """
-    Handles a redirect exception by navigating to the given path.
-
-    Args:
-        exception (RedirectException): The redirect exception containing the path to navigate to.
-    """
-    self.router.navigate_to_path(exception.path)
+378
+379
+380
+381
+382
+383
def handle_redirect(self, exception):
+    """
+    Handles a redirect exception by navigating to the given path.
+
+    Args:
+        exception (RedirectException): The redirect exception containing the path to navigate to.
+    """
+    self.router.navigate_to_path(exception.path)
 
@@ -2659,27 +2659,27 @@

Source code in puepy/application.py -
347
-348
-349
-350
-351
-352
+
def handle_unauthorized(self, exception):
-    """
-    Handles the exception for unauthorized page. By default, it mounts the self.unauthorized_page class and passes it
-    the exception as an argument.
-
-    Args:
-        exception (Exception): The exception that occurred.
-    """
-    self.mount_page(
-        self._selector_or_element, self.unauthorized_page, None, {"error": exception}, handle_exceptions=False
-    )
+357
+358
+359
+360
+361
+362
def handle_unauthorized(self, exception):
+    """
+    Handles the exception for unauthorized page. By default, it mounts the self.unauthorized_page class and passes it
+    the exception as an argument.
+
+    Args:
+        exception (Exception): The exception that occurred.
+    """
+    self.mount_page(
+        self._selector_or_element, self.unauthorized_page, None, {"error": exception}, handle_exceptions=False
+    )
 
@@ -2737,29 +2737,29 @@

Source code in puepy/application.py -
126
-127
-128
-129
-130
-131
+
def install_router(self, router_class, **kwargs):
-    """
-    Install a router in the application.
-
-    Args:
-        router_class (class): A class that implements the router logic for the application. At this time, only
-            `puepy.router.Router` is available.
-        **kwargs: Additional keyword arguments that can be passed to the router_class constructor.
-    """
-    self.router = router_class(application=self, **kwargs)
-    if not is_server_side:
-        add_event_listener(window, "popstate", self._on_popstate)
+137
+138
+139
+140
+141
+142
def install_router(self, router_class, **kwargs):
+    """
+    Install a router in the application.
+
+    Args:
+        router_class (class): A class that implements the router logic for the application. At this time, only
+            `puepy.router.Router` is available.
+        **kwargs: Additional keyword arguments that can be passed to the router_class constructor.
+    """
+    self.router = router_class(application=self, **kwargs)
+    if not is_server_side:
+        add_event_listener(window, "popstate", self._on_popstate)
 
@@ -2851,12 +2851,7 @@

Source code in puepy/application.py -
191
-192
-193
-194
-195
-196
+
196
 197
 198
 199
@@ -2899,55 +2894,60 @@ 

236 237 238 -239

def mount(self, selector_or_element, path=None, page_kwargs=None):
-    """
-    Mounts a page onto the specified selector or element with optional path and page_kwargs.
-
-    Args:
-        selector_or_element: The selector or element on which to mount the page.
-        path: Optional path to match against the router. Defaults to None.
-        page_kwargs: Optional keyword arguments to pass to the mounted page. Defaults to None.
+239
+240
+241
+242
+243
+244
def mount(self, selector_or_element, path=None, page_kwargs=None):
+    """
+    Mounts a page onto the specified selector or element with optional path and page_kwargs.
 
-    Returns:
-        (Page): The mounted page instance
-    """
-    if page_kwargs is None:
-        page_kwargs = {}
-
-    self._selector_or_element = selector_or_element
-
-    if self.router:
-        path = path or self.current_path
-        route, arguments = self.router.match(path)
-        if arguments:
-            page_kwargs.update(arguments)
-
-        if route:
-            page_class = route.page
-        elif path in ("", "/") and self.default_page:
-            page_class = self.default_page
-        elif self.not_found_page:
-            page_class = self.not_found_page
-        else:
-            return None
-    elif self.default_page:
-        route = None
-        page_class = self.default_page
-    else:
-        return None
-
-    self.active_page = None
-    try:
-        self.mount_page(
-            selector_or_element=selector_or_element,
-            page_class=page_class,
-            route=route,
-            page_kwargs=page_kwargs,
-            handle_exceptions=True,
-        )
-    except Exception as e:
-        self.handle_error(e)
-    return self.active_page
+    Args:
+        selector_or_element: The selector or element on which to mount the page.
+        path: Optional path to match against the router. Defaults to None.
+        page_kwargs: Optional keyword arguments to pass to the mounted page. Defaults to None.
+
+    Returns:
+        (Page): The mounted page instance
+    """
+    if page_kwargs is None:
+        page_kwargs = {}
+
+    self._selector_or_element = selector_or_element
+
+    if self.router:
+        path = path or self.current_path
+        route, arguments = self.router.match(path)
+        if arguments:
+            page_kwargs.update(arguments)
+
+        if route:
+            page_class = route.page
+        elif path in ("", "/") and self.default_page:
+            page_class = self.default_page
+        elif self.not_found_page:
+            page_class = self.not_found_page
+        else:
+            return None
+    elif self.default_page:
+        route = None
+        page_class = self.default_page
+    else:
+        return None
+
+    self.active_page = None
+    try:
+        self.mount_page(
+            selector_or_element=selector_or_element,
+            page_class=page_class,
+            route=route,
+            page_kwargs=page_kwargs,
+            handle_exceptions=True,
+        )
+    except Exception as e:
+        self.handle_error(e)
+    return self.active_page
 
@@ -3054,12 +3054,7 @@

Source code in puepy/application.py -
256
-257
-258
-259
-260
-261
+
261
 262
 263
 264
@@ -3090,43 +3085,48 @@ 

289 290 291 -292

def mount_page(self, selector_or_element, page_class, route, page_kwargs, handle_exceptions=True):
-    """
-    Mounts a page on the specified selector or element with the given parameters.
-
-    Args:
-        selector_or_element (str or Element): The selector string or element to mount the page on.
-        page_class (class): The page class to mount.
-        route (str): The route for the page.
-        page_kwargs (dict): Additional keyword arguments to pass to the page class.
-        handle_exceptions (bool, optional): Determines whether to handle exceptions thrown during mounting.
-            Defaults to True.
-    """
-    page_class._expanded_props()
-
-    # For security, we only pass props to the page that are defined in the page's props
-    #
-    # We also handle the list or not-list props for multiple or single values
-    # (eg, ?foo=1&foo=2 -> ["1", "2"] if needed)
-    #
-    prop_args = {}
-    prop: Prop
-    for prop in page_class.props_expanded.values():
-        if prop.name in page_kwargs:
-            value = page_kwargs.pop(prop.name)
-            if prop.type is list:
-                prop_args[prop.name] = value if isinstance(value, list) else [value]
-            else:
-                prop_args[prop.name] = value if not isinstance(value, list) else value[0]
-
-    self.active_page: Page = page_class(matched_route=route, application=self, extra_args=page_kwargs, **prop_args)
-    try:
-        self.active_page.mount(selector_or_element)
-    except exceptions.PageError as e:
-        if handle_exceptions:
-            self.handle_page_error(e)
-        else:
-            raise
+292
+293
+294
+295
+296
+297
def mount_page(self, selector_or_element, page_class, route, page_kwargs, handle_exceptions=True):
+    """
+    Mounts a page on the specified selector or element with the given parameters.
+
+    Args:
+        selector_or_element (str or Element): The selector string or element to mount the page on.
+        page_class (class): The page class to mount.
+        route (str): The route for the page.
+        page_kwargs (dict): Additional keyword arguments to pass to the page class.
+        handle_exceptions (bool, optional): Determines whether to handle exceptions thrown during mounting.
+            Defaults to True.
+    """
+    page_class._expanded_props()
+
+    # For security, we only pass props to the page that are defined in the page's props
+    #
+    # We also handle the list or not-list props for multiple or single values
+    # (eg, ?foo=1&foo=2 -> ["1", "2"] if needed)
+    #
+    prop_args = {}
+    prop: Prop
+    for prop in page_class.props_expanded.values():
+        if prop.name in page_kwargs:
+            value = page_kwargs.pop(prop.name)
+            if prop.type is list:
+                prop_args[prop.name] = value if isinstance(value, list) else [value]
+            else:
+                prop_args[prop.name] = value if not isinstance(value, list) else value[0]
+
+    self.active_page: Page = page_class(matched_route=route, application=self, extra_args=page_kwargs, **prop_args)
+    try:
+        self.active_page.mount(selector_or_element)
+    except exceptions.PageError as e:
+        if handle_exceptions:
+            self.handle_page_error(e)
+        else:
+            raise
 
@@ -3191,12 +3191,7 @@

Source code in puepy/application.py -
139
-140
-141
-142
-143
-144
+
144
 145
 146
 147
@@ -3224,40 +3219,45 @@ 

169 170 171 -172

def page(self, route=None, name=None):
-    """
-    A decorator for `Page` classes which adds the page to the application with a specified route and name.
-
-    Intended to be called as a decorator.
-
-    Args:
-        route (str): The route for the page. Default is None.
-        name (str): The name of the page. If left None, page class is used as the name.
-
-    Examples:
-        ``` py
-        app = Application()
-        @app.page("/my-page")
-        class MyPage(Page):
-            ...
-        ```
-    """
-    if route:
-        if not self.router:
-            raise Exception("Router not installed")
-
-        def decorator(func):
-            self.router.add_route(route, func, name=name)
-            return func
-
-        return decorator
-    else:
-
-        def decorator(func):
-            self.default_page = func
-            return func
-
-        return decorator
+172
+173
+174
+175
+176
+177
def page(self, route=None, name=None):
+    """
+    A decorator for `Page` classes which adds the page to the application with a specified route and name.
+
+    Intended to be called as a decorator.
+
+    Args:
+        route (str): The route for the page. Default is None.
+        name (str): The name of the page. If left None, page class is used as the name.
+
+    Examples:
+        ``` py
+        app = Application()
+        @app.page("/my-page")
+        class MyPage(Page):
+            ...
+        ```
+    """
+    if route:
+        if not self.router:
+            raise Exception("Router not installed")
+
+        def decorator(func):
+            self.router.add_route(route, func, name=name)
+            return func
+
+        return decorator
+    else:
+
+        def decorator(func):
+            self.default_page = func
+            return func
+
+        return decorator
 
@@ -3315,25 +3315,25 @@

Source code in puepy/application.py -
180
-181
-182
-183
-184
-185
+
def remount(self, path=None, page_kwargs=None):
-    """
-    Remounts the selected element or selector with the specified path and page_kwargs.
-
-    Args:
-        path (str): The new path to be used for remounting the element or selector. Default is None.
-        page_kwargs (dict): Additional page kwargs to be passed when remounting. Default is None.
-
-    """
-    self.mount(self._selector_or_element, path=path, page_kwargs=page_kwargs)
+189
+190
+191
+192
+193
+194
def remount(self, path=None, page_kwargs=None):
+    """
+    Remounts the selected element or selector with the specified path and page_kwargs.
+
+    Args:
+        path (str): The new path to be used for remounting the element or selector. Default is None.
+        page_kwargs (dict): Additional page kwargs to be passed when remounting. Default is None.
+
+    """
+    self.mount(self._selector_or_element, path=path, page_kwargs=page_kwargs)
 
diff --git a/development/reference/component/index.html b/development/reference/component/index.html index 27d7dc6..47467b5 100644 --- a/development/reference/component/index.html +++ b/development/reference/component/index.html @@ -1446,7 +1446,24 @@

puepy.Component

Source code in puepy/core.py -
661
+
644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
+658
+659
+660
+661
 662
 663
 664
@@ -1571,166 +1588,149 @@ 

puepy.Component

783 784 785 -786 -787 -788 -789 -790 -791 -792 -793 -794 -795 -796 -797 -798 -799 -800 -801 -802 -803
class Component(Tag, Stateful):
-    """
-    Components are a way of defining reusable and composable elements in PuePy. They are a subclass of Tag, but provide
-    additional features such as state management and props. By defining your own components and registering them, you
-    can create a library of reusable elements for your application.
-
-    Attributes:
-        enclosing_tag (str): The tag name that will enclose the component. To be defined as a class attribute on subclasses.
-        component_name (str): The name of the component. If left blank, class name is used. To be defined as a class attribute on subclasses.
-        redraw_on_state_changes (bool): Whether the component should redraw when its state changes. To be defined as a class attribute on subclasses.
-        redraw_on_app_state_changes (bool): Whether the component should redraw when the application state changes. To be defined as a class attribute on subclasses.
-        props (list): A list of props for the component. To be defined as a class attribute on subclasses.
-    """
+786
class Component(Tag, Stateful):
+    """
+    Components are a way of defining reusable and composable elements in PuePy. They are a subclass of Tag, but provide
+    additional features such as state management and props. By defining your own components and registering them, you
+    can create a library of reusable elements for your application.
+
+    Attributes:
+        enclosing_tag (str): The tag name that will enclose the component. To be defined as a class attribute on subclasses.
+        component_name (str): The name of the component. If left blank, class name is used. To be defined as a class attribute on subclasses.
+        redraw_on_state_changes (bool): Whether the component should redraw when its state changes. To be defined as a class attribute on subclasses.
+        redraw_on_app_state_changes (bool): Whether the component should redraw when the application state changes. To be defined as a class attribute on subclasses.
+        props (list): A list of props for the component. To be defined as a class attribute on subclasses.
+    """
+
+    enclosing_tag = "div"
+    component_name = None
+    redraw_on_state_changes = True
+    redraw_on_app_state_changes = True
+
+    props = []
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, tag_name=self.enclosing_tag, **kwargs)
+        self.state = ReactiveDict(self.initial())
+        self.add_context("state", self.state)
+
+        self.slots = {}
+
+    def _handle_attrs(self, kwargs):
+        self._handle_props(kwargs)
 
-    enclosing_tag = "div"
-    component_name = None
-    redraw_on_state_changes = True
-    redraw_on_app_state_changes = True
-
-    props = []
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, tag_name=self.enclosing_tag, **kwargs)
-        self.state = ReactiveDict(self.initial())
-        self.add_context("state", self.state)
+        super()._handle_attrs(kwargs)
+
+    def _handle_props(self, kwargs):
+        if not hasattr(self, "props_expanded"):
+            self._expanded_props()
+
+        self.props_values = {}
+        for name, prop in self.props_expanded.items():
+            value = kwargs.pop(prop.name, prop.default_value)
+            setattr(self, name, value)
+            self.props_values[name] = value
 
-        self.slots = {}
-
-    def _handle_attrs(self, kwargs):
-        self._handle_props(kwargs)
-
-        super()._handle_attrs(kwargs)
-
-    def _handle_props(self, kwargs):
-        if not hasattr(self, "props_expanded"):
-            self._expanded_props()
-
-        self.props_values = {}
-        for name, prop in self.props_expanded.items():
-            value = kwargs.pop(prop.name, prop.default_value)
-            setattr(self, name, value)
-            self.props_values[name] = value
-
-    @classmethod
-    def _expanded_props(cls):
-        # This would be ideal for metaprogramming, but we do it this way to be compatible with Micropython. :/
-        props_expanded = {}
-        for prop in cls.props:
-            if isinstance(prop, Prop):
-                props_expanded[prop.name] = prop
-            elif isinstance(prop, dict):
-                props_expanded[prop["name"]] = Prop(**prop)
-            elif isinstance(prop, str):
-                props_expanded[prop] = Prop(name=prop)
-            else:
-                raise PropsError(f"Unknown prop type {type(prop)}")
-        cls.props_expanded = props_expanded
-
-    def initial(self):
-        """
-        To be overridden in subclasses, the `initial()` method defines the initial state of the component.
-
-        Returns:
-            (dict): Initial component state
-        """
-        return {}
-
-    def _on_state_change(self, context, key, value):
-        super()._on_state_change(context, key, value)
+    @classmethod
+    def _expanded_props(cls):
+        # This would be ideal for metaprogramming, but we do it this way to be compatible with Micropython. :/
+        props_expanded = {}
+        for prop in cls.props:
+            if isinstance(prop, Prop):
+                props_expanded[prop.name] = prop
+            elif isinstance(prop, dict):
+                props_expanded[prop["name"]] = Prop(**prop)
+            elif isinstance(prop, str):
+                props_expanded[prop] = Prop(name=prop)
+            else:
+                raise PropsError(f"Unknown prop type {type(prop)}")
+        cls.props_expanded = props_expanded
+
+    def initial(self):
+        """
+        To be overridden in subclasses, the `initial()` method defines the initial state of the component.
+
+        Returns:
+            (dict): Initial component state
+        """
+        return {}
+
+    def _on_state_change(self, context, key, value):
+        super()._on_state_change(context, key, value)
+
+        if context == "state":
+            redraw_rule = self.redraw_on_state_changes
+        elif context == "app":
+            redraw_rule = self.redraw_on_app_state_changes
+        else:
+            return
+
+        if redraw_rule is True:
+            self.page.redraw_tag(self)
+        elif redraw_rule is False:
+            pass
+        elif isinstance(redraw_rule, (list, set)):
+            if key in redraw_rule:
+                self.page.redraw_tag(self)
+        else:
+            raise Exception(f"Unknown value for redraw rule: {redraw_rule} (context: {context})")
 
-        if context == "state":
-            redraw_rule = self.redraw_on_state_changes
-        elif context == "app":
-            redraw_rule = self.redraw_on_app_state_changes
-        else:
-            return
-
-        if redraw_rule is True:
-            self.page.redraw_tag(self)
-        elif redraw_rule is False:
-            pass
-        elif isinstance(redraw_rule, (list, set)):
-            if key in redraw_rule:
-                self.page.redraw_tag(self)
-        else:
-            raise Exception(f"Unknown value for redraw rule: {redraw_rule} (context: {context})")
-
-    def insert_slot(self, name="default", **kwargs):
-        """
-        In defining your own component, when you want to create a slot in your `populate` method, you can use this method.
-
-        Args:
-            name (str): The name of the slot. If not passed, the default slot is inserted.
-            **kwargs: Additional keyword arguments to be passed to Slot initialization.
-
-        Returns:
-            Slot: The inserted slot object.
-        """
-        if name in self.slots:
-            self.slots[name].parent = Tag.stack[-1]  # The children will be cleared during redraw, so re-establish
-        else:
-            self.slots[name] = Slot(ref=f"slot={name}", slot_name=name, page=self.page, parent=Tag.stack[-1], **kwargs)
-        slot = self.slots[name]
-        if self.origin:
-            slot.origin = self.origin
-            if slot.ref:
-                self.origin.refs[slot.ref] = slot
-        return slot
+    def insert_slot(self, name="default", **kwargs):
+        """
+        In defining your own component, when you want to create a slot in your `populate` method, you can use this method.
+
+        Args:
+            name (str): The name of the slot. If not passed, the default slot is inserted.
+            **kwargs: Additional keyword arguments to be passed to Slot initialization.
+
+        Returns:
+            Slot: The inserted slot object.
+        """
+        if name in self.slots:
+            self.slots[name].parent = Tag.stack[-1]  # The children will be cleared during redraw, so re-establish
+        else:
+            self.slots[name] = Slot(ref=f"slot={name}", slot_name=name, page=self.page, parent=Tag.stack[-1], **kwargs)
+        slot = self.slots[name]
+        if self.origin:
+            slot.origin = self.origin
+            if slot.ref:
+                self.origin.refs[slot.ref] = slot
+        return slot
+
+    def slot(self, name="default"):
+        """
+        To be used in the `populate` method of code making use of this component, this method returns the slot object
+        with the given name. It should be used inside of a context manager.
+
+        Args:
+            name (str): The name of the slot to clear and return.
+
+        Returns:
+            Slot: The cleared slot object.
+        """
+        #
+        # We put this here, so it clears the children only when the slot-filler is doing its filling.
+        # Otherwise, the previous children are kept. Lucky them.
+        self.slots[name].children = []
+        return self.slots[name]
 
-    def slot(self, name="default"):
-        """
-        To be used in the `populate` method of code making use of this component, this method returns the slot object
-        with the given name. It should be used inside of a context manager.
-
-        Args:
-            name (str): The name of the slot to clear and return.
-
-        Returns:
-            Slot: The cleared slot object.
-        """
-        #
-        # We put this here, so it clears the children only when the slot-filler is doing its filling.
-        # Otherwise, the previous children are kept. Lucky them.
-        self.slots[name].children = []
-        return self.slots[name]
-
-    def __enter__(self):
-        self.stack.append(self)
-        self.origin_stack[0].append(self)
-        self.component_stack.append(self)
-        return self
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        self.stack.pop()
-        self.origin_stack[0].pop()
-        self.component_stack.pop()
-        return False
-
-    def __str__(self):
-        return f"{self.component_name or self.__class__.__name__} ({self.ref} {id(self)})"
-
-    def __repr__(self):
-        return f"<{self}>"
+    def __enter__(self):
+        self.stack.append(self)
+        self.origin_stack[0].append(self)
+        self.component_stack.append(self)
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.stack.pop()
+        self.origin_stack[0].pop()
+        self.component_stack.pop()
+        return False
+
+    def __str__(self):
+        return f"{self.component_name or self.__class__.__name__} ({self.ref} {id(self)})"
+
+    def __repr__(self):
+        return f"<{self}>"
 
@@ -1763,21 +1763,21 @@

Source code in puepy/core.py -
def initial(self):
-    """
-    To be overridden in subclasses, the `initial()` method defines the initial state of the component.
-
-    Returns:
-        (dict): Initial component state
-    """
-    return {}
+
def initial(self):
+    """
+    To be overridden in subclasses, the `initial()` method defines the initial state of the component.
+
+    Returns:
+        (dict): Initial component state
+    """
+    return {}
 
@@ -1854,47 +1854,47 @@

Source code in puepy/core.py -
748
+
def insert_slot(self, name="default", **kwargs):
-    """
-    In defining your own component, when you want to create a slot in your `populate` method, you can use this method.
-
-    Args:
-        name (str): The name of the slot. If not passed, the default slot is inserted.
-        **kwargs: Additional keyword arguments to be passed to Slot initialization.
-
-    Returns:
-        Slot: The inserted slot object.
-    """
-    if name in self.slots:
-        self.slots[name].parent = Tag.stack[-1]  # The children will be cleared during redraw, so re-establish
-    else:
-        self.slots[name] = Slot(ref=f"slot={name}", slot_name=name, page=self.page, parent=Tag.stack[-1], **kwargs)
-    slot = self.slots[name]
-    if self.origin:
-        slot.origin = self.origin
-        if slot.ref:
-            self.origin.refs[slot.ref] = slot
-    return slot
+751
def insert_slot(self, name="default", **kwargs):
+    """
+    In defining your own component, when you want to create a slot in your `populate` method, you can use this method.
+
+    Args:
+        name (str): The name of the slot. If not passed, the default slot is inserted.
+        **kwargs: Additional keyword arguments to be passed to Slot initialization.
+
+    Returns:
+        Slot: The inserted slot object.
+    """
+    if name in self.slots:
+        self.slots[name].parent = Tag.stack[-1]  # The children will be cleared during redraw, so re-establish
+    else:
+        self.slots[name] = Slot(ref=f"slot={name}", slot_name=name, page=self.page, parent=Tag.stack[-1], **kwargs)
+    slot = self.slots[name]
+    if self.origin:
+        slot.origin = self.origin
+        if slot.ref:
+            self.origin.refs[slot.ref] = slot
+    return slot
 
@@ -1957,37 +1957,37 @@

Source code in puepy/core.py -
def slot(self, name="default"):
-    """
-    To be used in the `populate` method of code making use of this component, this method returns the slot object
-    with the given name. It should be used inside of a context manager.
-
-    Args:
-        name (str): The name of the slot to clear and return.
-
-    Returns:
-        Slot: The cleared slot object.
-    """
-    #
-    # We put this here, so it clears the children only when the slot-filler is doing its filling.
-    # Otherwise, the previous children are kept. Lucky them.
-    self.slots[name].children = []
-    return self.slots[name]
+
def slot(self, name="default"):
+    """
+    To be used in the `populate` method of code making use of this component, this method returns the slot object
+    with the given name. It should be used inside of a context manager.
+
+    Args:
+        name (str): The name of the slot to clear and return.
+
+    Returns:
+        Slot: The cleared slot object.
+    """
+    #
+    # We put this here, so it clears the children only when the slot-filler is doing its filling.
+    # Otherwise, the previous children are kept. Lucky them.
+    self.slots[name].children = []
+    return self.slots[name]
 
diff --git a/development/reference/tag/index.html b/development/reference/tag/index.html index 414796f..d8172cf 100644 --- a/development/reference/tag/index.html +++ b/development/reference/tag/index.html @@ -986,15 +986,6 @@ - - -
  • - - - _add_event_listener - - -
  • @@ -1385,15 +1376,6 @@ -
  • - -
  • - - - _add_event_listener - - -
  • @@ -2243,24 +2225,7 @@

    puepy.core.Tag

    629 630 631 -632 -633 -634 -635 -636 -637 -638 -639 -640 -641 -642 -643 -644 -645 -646 -647 -648 -649
  • class Tag:
    +632
    class Tag:
         """
         The most basic building block of a PuePy app. A Tag is a single HTML element. This is also the base class of
         `Component`, which is then the base class of `Page`.
    @@ -2545,7 +2510,7 @@ 

    puepy.core.Tag

    element.value = value element.setAttribute("value", value) event_type = "input" - self._add_event_listener(element, event_type, self.on_bind_input) + self.add_event_listener(element, event_type, self.on_bind_input) elif self.bind: raise Exception("Cannot specify bind a valid parent component") @@ -2556,9 +2521,9 @@

    puepy.core.Tag

    key = key.replace("_", "-") if isinstance(value, (list, tuple)): for handler in value: - self._add_event_listener(element, key, handler) + self.add_event_listener(element, key, handler) else: - self._add_event_listener(element, key, value) + self.add_event_listener(element, key, value) def render_children(self, element): for child in self.children: @@ -2609,7 +2574,7 @@

    puepy.core.Tag

    def get_default_attrs(self): return self.default_attrs.copy() - def _add_event_listener(self, element, event, listener): + def add_event_listener(self, element, event, listener): """ Just an internal wrapper around add_event_listener (JS function) that keeps track of what we added, so we can garbage collect it later. @@ -2620,239 +2585,253 @@

    puepy.core.Tag

    if not is_server_side: add_event_listener(element, event, listener) - def add_event_listener(self, event, handler): - """ - Add an event listener for a given event. - - Args: - event (str): The name of the event to listen for. - handler (function): The function to be executed when the event occurs. - - """ - if event not in self._manually_added_event_listeners: - self._manually_added_event_listeners[event] = handler - else: - existing_handler = self._manually_added_event_listeners[event] - if isinstance(existing_handler, (list, tuple)): - self._manually_added_event_listeners[event] = [existing_handler] + list(handler) - else: - self._manually_added_event_listeners[event] = [existing_handler, handler] - if self._rendered_element: - self._add_event_listener(self._rendered_element, event, handler) - - def mount(self, selector_or_element): - self.update_title() - if not self._children_generated: - with self: - self.generate_children() - - if isinstance(selector_or_element, str): - element = self.document.querySelector(selector_or_element) - else: - element = selector_or_element - - if not element: - raise RuntimeError(f"Element {selector_or_element} not found") - - element.innerHTML = "" - element.appendChild(self.render()) - self.recursive_call("on_ready") - self.add_python_css_classes() + def mount(self, selector_or_element): + self.update_title() + if not self._children_generated: + with self: + self.generate_children() + + if isinstance(selector_or_element, str): + element = self.document.querySelector(selector_or_element) + else: + element = selector_or_element + + if not element: + raise RuntimeError(f"Element {selector_or_element} not found") + + element.innerHTML = "" + element.appendChild(self.render()) + self.recursive_call("on_ready") + self.add_python_css_classes() + + def add_python_css_classes(self): + """ + This is only done at the page level. + """ + pass + + def recursive_call(self, method, *args, **kwargs): + """ + Recursively call a specified method on all child Tag objects. + + Args: + method (str): The name of the method to be called on each Tag object. + *args: Optional arguments to be passed to the method. + **kwargs: Optional keyword arguments to be passed to the method. + """ + for child in self.children: + if isinstance(child, Tag): + child.recursive_call(method, *args, **kwargs) + getattr(self, method)(*args, **kwargs) - def add_python_css_classes(self): - """ - This is only done at the page level. - """ - pass - - def recursive_call(self, method, *args, **kwargs): - """ - Recursively call a specified method on all child Tag objects. - - Args: - method (str): The name of the method to be called on each Tag object. - *args: Optional arguments to be passed to the method. - **kwargs: Optional keyword arguments to be passed to the method. - """ - for child in self.children: - if isinstance(child, Tag): - child.recursive_call(method, *args, **kwargs) - getattr(self, method)(*args, **kwargs) - - def on_ready(self): - pass - - def _retain_implicit_attrs(self): - """ - Retain attributes set elsewhere - """ - for attr in self.element.attributes: - if attr.name not in self.attrs and attr.name != "id": - self._retained_attrs[attr.name] = attr.value - - def on_redraw(self): - pass - - def on_bind_input(self, event): - input_type = _element_input_type(event.target) - if input_type == "checkbox": - self.set_bind_value(self.bind, event.target.checked) - elif input_type == "radio": - if event.target.checked: - self.set_bind_value(self.bind, event.target.value) - elif input_type == "number": - value = event.target.value - try: - if "." in str(value): - value = float(value) - else: - value = int(value) - except (ValueError, TypeError): - pass - self.set_bind_value(self.bind, value) - else: - self.set_bind_value(self.bind, event.target.value) + def on_ready(self): + pass + + def _retain_implicit_attrs(self): + """ + Retain attributes set elsewhere + """ + try: + for attr in self.element.attributes: + if attr.name not in self.attrs and attr.name != "id": + self._retained_attrs[attr.name] = attr.value + except ElementNotInDom: + pass + + def on_redraw(self): + pass + + def on_bind_input(self, event): + input_type = _element_input_type(event.target) + if input_type == "checkbox": + self.set_bind_value(self.bind, event.target.checked) + elif input_type == "radio": + if event.target.checked: + self.set_bind_value(self.bind, event.target.value) + elif input_type == "number": + value = event.target.value + try: + if "." in str(value): + value = float(value) + else: + value = int(value) + except (ValueError, TypeError): + pass + self.set_bind_value(self.bind, value) + else: + self.set_bind_value(self.bind, event.target.value) + + def set_bind_value(self, bind, value): + if type(bind) in (list, tuple): + nested_dict = self.origin.state + for key in bind[:-1]: + nested_dict = nested_dict[key] + with self.origin.state.mutate(bind[0]): + nested_dict[bind[-1]] = value + else: + self.origin.state[self.bind] = value + + @property + def page(self): + if self._page: + return self._page + elif isinstance(self, Page): + return self - def set_bind_value(self, bind, value): - if type(bind) in (list, tuple): - nested_dict = self.origin.state - for key in bind[:-1]: - nested_dict = nested_dict[key] - with self.origin.state.mutate(bind[0]): - nested_dict[bind[-1]] = value - else: - self.origin.state[self.bind] = value - - @property - def page(self): - if self._page: - return self._page - elif isinstance(self, Page): - return self + @property + def router(self): + if self.application: + return self.application.router + + @property + def parent(self): + return self._parent + + @parent.setter + def parent(self, new_parent): + existing_parent = getattr(self, "_parent", None) + if new_parent == existing_parent: + if new_parent and self not in new_parent.children: + existing_parent.children.append(self) + return - @property - def router(self): - if self.application: - return self.application.router + if existing_parent and self in existing_parent.children: + existing_parent.children.remove(self) + if new_parent and self not in new_parent.children: + new_parent.children.append(self) - @property - def parent(self): - return self._parent - - @parent.setter - def parent(self, new_parent): - existing_parent = getattr(self, "_parent", None) - if new_parent == existing_parent: - if new_parent and self not in new_parent.children: - existing_parent.children.append(self) - return - - if existing_parent and self in existing_parent.children: - existing_parent.children.remove(self) - if new_parent and self not in new_parent.children: - new_parent.children.append(self) - - self._parent = new_parent - - def add(self, *children): - for child in children: - if isinstance(child, Tag): - child.parent = self - else: - self.children.append(child) - - def redraw(self): - if self in self.page.redraw_list: - self.page.redraw_list.remove(self) + self._parent = new_parent + + def add(self, *children): + for child in children: + if isinstance(child, Tag): + child.parent = self + else: + self.children.append(child) + + def redraw(self): + if self in self.page.redraw_list: + self.page.redraw_list.remove(self) + + try: + element = self.element + except ElementNotInDom: + return + + if is_server_side: + old_active_element_id = None + else: + old_active_element_id = self.document.activeElement.id if self.document.activeElement else None + + self.recursive_call("_retain_implicit_attrs") + + self.children = [] + + attrs = self.get_default_attrs() + attrs.update(self.attrs) - try: - element = self.element - except ElementNotInDom: - return - - if is_server_side: - old_active_element_id = None - else: - old_active_element_id = self.document.activeElement.id if self.document.activeElement else None + self.update_title() + with self: + self.generate_children() + + staging_element = self._create_element(attrs) + + self._render_onto(staging_element, attrs) + + patch_dom_element(staging_element, element) - self.recursive_call("_retain_implicit_attrs") - - self.children = [] - - attrs = self.get_default_attrs() - attrs.update(self.attrs) + if old_active_element_id is not None: + el = self.document.getElementById(old_active_element_id) + if el: + el.focus() + + self.recursive_call("on_redraw") - self.update_title() - with self: - self.generate_children() + def trigger_event(self, event, detail=None, **kwargs): + """ + Triggers an event to be consumed by code using this class. - staging_element = self._create_element(attrs) - - self._render_onto(staging_element, attrs) - - patch_dom_element(staging_element, element) - - if old_active_element_id is not None: - el = self.document.getElementById(old_active_element_id) - if el: - el.focus() + Args: + event (str): The name of the event to trigger. If the event name contains underscores, a warning message is printed suggesting to use dashes instead. + detail (dict, optional): Additional data to be sent with the event. This should be a dictionary where the keys and values will be converted to JavaScript objects. + **kwargs: Additional keyword arguments. These arguments are not used in the implementation of the method and are ignored. + ß""" + if "_" in event: + print("Triggering event with underscores. Did you mean dashes?: ", event) + + # noinspection PyUnresolvedReferences + from pyscript.ffi import to_js - self.recursive_call("on_redraw") - - def trigger_event(self, event, detail=None, **kwargs): - """ - Triggers an event to be consumed by code using this class. - - Args: - event (str): The name of the event to trigger. If the event name contains underscores, a warning message is printed suggesting to use dashes instead. - detail (dict, optional): Additional data to be sent with the event. This should be a dictionary where the keys and values will be converted to JavaScript objects. - **kwargs: Additional keyword arguments. These arguments are not used in the implementation of the method and are ignored. - ß""" - if "_" in event: - print("Triggering event with underscores. Did you mean dashes?: ", event) + # noinspection PyUnresolvedReferences + from js import Object, Map + + if detail: + event_object = to_js({"detail": Map.new(Object.entries(to_js(detail)))}) + else: + event_object = to_js({}) + + self.element.dispatchEvent(CustomEvent.new(event, event_object)) + + def update_title(self): + """ + To be overridden by subclasses (usually pages), this method should update the Window title as needed. - # noinspection PyUnresolvedReferences - from pyscript.ffi import to_js - - # noinspection PyUnresolvedReferences - from js import Object, Map - - if detail: - event_object = to_js({"detail": Map.new(Object.entries(to_js(detail)))}) - else: - event_object = to_js({}) - - self.element.dispatchEvent(CustomEvent.new(event, event_object)) - - def update_title(self): - """ - To be overridden by subclasses (usually pages), this method should update the Window title as needed. + Called on mounting or redraw. + """ + pass + + def __enter__(self): + self.stack.append(self) + self.origin_stack[0].append(self) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stack.pop() + self.origin_stack[0].pop() + return False + + def __str__(self): + return self.tag_name - Called on mounting or redraw. - """ - pass - - def __enter__(self): - self.stack.append(self) - self.origin_stack[0].append(self) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.stack.pop() - self.origin_stack[0].pop() - return False - - def __str__(self): - return self.tag_name - - def __repr__(self): - return f"<{self} ({id(self)})>" + def __repr__(self): + return f"<{self} ({id(self)})>"
    -

    -_add_event_listener(element, event, listener) +

    +_retain_implicit_attrs() +

    +
    +

    Retain attributes set elsewhere

    +
    +Source code in puepy/core.py +
    def _retain_implicit_attrs(self):
    +    """
    +    Retain attributes set elsewhere
    +    """
    +    try:
    +        for attr in self.element.attributes:
    +            if attr.name not in self.attrs and attr.name != "id":
    +                self._retained_attrs[attr.name] = attr.value
    +    except ElementNotInDom:
    +        pass
    +
    +
    +
    +
    +
    +

    +add_event_listener(element, event, listener)

    Just an internal wrapper around add_event_listener (JS function) that keeps track of what we added, so @@ -2869,7 +2848,7 @@

    418 419 420 -421

    def _add_event_listener(self, element, event, listener):
    +421
    def add_event_listener(self, element, event, listener):
         """
         Just an internal wrapper around add_event_listener (JS function) that keeps track of what we added, so
         we can garbage collect it later.
    @@ -2884,125 +2863,6 @@ 

    -

    -_retain_implicit_attrs() -

    -
    -

    Retain attributes set elsewhere

    -
    -Source code in puepy/core.py -
    def _retain_implicit_attrs(self):
    -    """
    -    Retain attributes set elsewhere
    -    """
    -    for attr in self.element.attributes:
    -        if attr.name not in self.attrs and attr.name != "id":
    -            self._retained_attrs[attr.name] = attr.value
    -
    -
    -
    -
    -
    -

    -add_event_listener(event, handler) -

    -
    -

    Add an event listener for a given event.

    -

    Parameters:

    - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionDefault
    -event - -str - -
    -

    The name of the event to listen for.

    -
    -
    -required -
    -handler - -function - -
    -

    The function to be executed when the event occurs.

    -
    -
    -required -
    -
    -Source code in puepy/core.py -
    def add_event_listener(self, event, handler):
    -    """
    -    Add an event listener for a given event.
    -
    -    Args:
    -        event (str): The name of the event to listen for.
    -        handler (function): The function to be executed when the event occurs.
    -
    -    """
    -    if event not in self._manually_added_event_listeners:
    -        self._manually_added_event_listeners[event] = handler
    -    else:
    -        existing_handler = self._manually_added_event_listeners[event]
    -        if isinstance(existing_handler, (list, tuple)):
    -            self._manually_added_event_listeners[event] = [existing_handler] + list(handler)
    -        else:
    -            self._manually_added_event_listeners[event] = [existing_handler, handler]
    -    if self._rendered_element:
    -        self._add_event_listener(self._rendered_element, event, handler)
    -
    -
    -
    -
    -

    add_python_css_classes()

    @@ -3010,15 +2870,15 @@

    This is only done at the page level.

    Source code in puepy/core.py -
    def add_python_css_classes(self):
    -    """
    -    This is only done at the page level.
    -    """
    -    pass
    +
    def add_python_css_classes(self):
    +    """
    +    This is only done at the page level.
    +    """
    +    pass
     
    @@ -3250,31 +3110,31 @@

    Source code in puepy/core.py -
    def recursive_call(self, method, *args, **kwargs):
    -    """
    -    Recursively call a specified method on all child Tag objects.
    -
    -    Args:
    -        method (str): The name of the method to be called on each Tag object.
    -        *args: Optional arguments to be passed to the method.
    -        **kwargs: Optional keyword arguments to be passed to the method.
    -    """
    -    for child in self.children:
    -        if isinstance(child, Tag):
    -            child.recursive_call(method, *args, **kwargs)
    -    getattr(self, method)(*args, **kwargs)
    +
    def recursive_call(self, method, *args, **kwargs):
    +    """
    +    Recursively call a specified method on all child Tag objects.
    +
    +    Args:
    +        method (str): The name of the method to be called on each Tag object.
    +        *args: Optional arguments to be passed to the method.
    +        **kwargs: Optional keyword arguments to be passed to the method.
    +    """
    +    for child in self.children:
    +        if isinstance(child, Tag):
    +            child.recursive_call(method, *args, **kwargs)
    +    getattr(self, method)(*args, **kwargs)
     
    @@ -3315,53 +3175,53 @@

    ß

    Source code in puepy/core.py -
    602
    +
    def trigger_event(self, event, detail=None, **kwargs):
    -    """
    -            Triggers an event to be consumed by code using this class.
    -
    -            Args:
    -                event (str): The name of the event to trigger. If the event name contains underscores, a warning message is printed suggesting to use dashes instead.
    -                detail (dict, optional): Additional data to be sent with the event. This should be a dictionary where the keys and values will be converted to JavaScript objects.
    -                **kwargs: Additional keyword arguments. These arguments are not used in the implementation of the method and are ignored.
    -    ß"""
    -    if "_" in event:
    -        print("Triggering event with underscores. Did you mean dashes?: ", event)
    -
    -    # noinspection PyUnresolvedReferences
    -    from pyscript.ffi import to_js
    -
    -    # noinspection PyUnresolvedReferences
    -    from js import Object, Map
    -
    -    if detail:
    -        event_object = to_js({"detail": Map.new(Object.entries(to_js(detail)))})
    -    else:
    -        event_object = to_js({})
    -
    -    self.element.dispatchEvent(CustomEvent.new(event, event_object))
    +608
    def trigger_event(self, event, detail=None, **kwargs):
    +    """
    +            Triggers an event to be consumed by code using this class.
    +
    +            Args:
    +                event (str): The name of the event to trigger. If the event name contains underscores, a warning message is printed suggesting to use dashes instead.
    +                detail (dict, optional): Additional data to be sent with the event. This should be a dictionary where the keys and values will be converted to JavaScript objects.
    +                **kwargs: Additional keyword arguments. These arguments are not used in the implementation of the method and are ignored.
    +    ß"""
    +    if "_" in event:
    +        print("Triggering event with underscores. Did you mean dashes?: ", event)
    +
    +    # noinspection PyUnresolvedReferences
    +    from pyscript.ffi import to_js
    +
    +    # noinspection PyUnresolvedReferences
    +    from js import Object, Map
    +
    +    if detail:
    +        event_object = to_js({"detail": Map.new(Object.entries(to_js(detail)))})
    +    else:
    +        event_object = to_js({})
    +
    +    self.element.dispatchEvent(CustomEvent.new(event, event_object))
     
    @@ -3375,19 +3235,19 @@

    Called on mounting or redraw.

    Source code in puepy/core.py -
    def update_title(self):
    -    """
    -    To be overridden by subclasses (usually pages), this method should update the Window title as needed.
    -
    -    Called on mounting or redraw.
    -    """
    -    pass
    +
    def update_title(self):
    +    """
    +    To be overridden by subclasses (usually pages), this method should update the Window title as needed.
    +
    +    Called on mounting or redraw.
    +    """
    +    pass
     
    diff --git a/development/search/search_index.json b/development/search/search_index.json index 3d20b32..1c48d2d 100644 --- a/development/search/search_index.json +++ b/development/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"PuePy: Overview","text":"

    PuePy is a frontend web framework that builds on Python and Webassembly using PyScript. PuePy is truly a Python-first development environment. There is no transpiling to JavaScript; no Yarn, no NPM, no webpack, no Vite or Parcel. Python runs directly in your browser. PuePy is inspired by Vue.js, but is built entirely from scratch in Python.

    "},{"location":"#features","title":"Features","text":"
    • Reactivity: As components' state changes, redraws happen automatically
    • Component-Based Design: Encapsulate data, logic, and presentation in reusable components
    • Single-Class Components: Based vaguely on Vue's \"single file components\", each component and each page is a class.
    • Events, Slots, and Props: Events, slots, and props are all inspired by Vue and work similarly in PuePy
    • Minimal, Python: PuePy is built to use Pythonic conventions whenever possible, and eschews verbosity in the name of opinion.
    "},{"location":"#external-links","title":"External Links","text":"

    PuePy.dev Main Site GitHub

    "},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#philosophical-questions","title":"Philosophical Questions","text":""},{"location":"faq/#why-not-just-use-javascript","title":"Why not just use Javascript?","text":"

    If you prefer JavaScript to Python, using it would be the obvious answer. JavaScript has mature tooling available, there are many excellent frameworks to choose from, and the JavaScript runtimes available in most browsers are blazingly fast.

    Some developers prefer Python, however. For them, PuePy might be a good choice.

    "},{"location":"faq/#is-webassembly-ready","title":"Is WebAssembly ready?","text":"

    WebAssembly is supported in all major modern browsers, including Safari. Its standard has coalesced and for using existing JavaScript libraries or components, PyScript provides a robust bridge. WebAssembly is as ready as it needs to be and is certainly less prone to backwards incompatible changes than many JavaScript projects that production sites rely on.

    "},{"location":"faq/#puepy-design-choices","title":"PuePy Design Choices","text":""},{"location":"faq/#can-you-use-puepy-with-a-templating-language-instead-of-building-components-inline","title":"Can you use PuePy with a templating language instead of building components inline?","text":"

    The idea behind PuePy is, at least in part, to have the convenience of building all your software, including its UI, out in Python's syntax. You may actually find that Python is more succinct, not less, than a similar template might be. Consider:

    <h1>{{ name }}'s grocery shopping list</h1>\n<ul>\n\n</ul>\n<button on_click=\"buy()\">Buy Items</button>\n

    vs:

    with t.h1():\n    t(f\"{name}'s grocery shopping list\")\nwith t.ul():\n    for item in items:\n       t.li(item)\nt.button(\"Buy Items\", on_click=self.buy)\n

    If you have a whole HTML file ready to go, try out the HTML to Python converter built in the PypII libraries tutorial chapter, which uses BeautifulSoup.

    "},{"location":"faq/#can-i-use-svgs","title":"Can I use SVGs?","text":"

    Yes, as long as you specify xmlns:

    with t.svg(xmlns=\"http://www.w3.org/2000/svg\"):\n    ...\n
    "},{"location":"faq/#how-can-i-use-html-directly","title":"How can I use HTML directly?","text":"

    If you want to directly insert HTML into a component's rendering, you can use the html() string:

    from puepy.core import html\n\n\nclass MyPage(Page):\n    def populate(self):\n        t(html(\"<strong>Hello!</strong>\"))\n
    "},{"location":"installation/","title":"Installation","text":""},{"location":"installation/#client-side-installation","title":"Client-side installation","text":"

    Although PuePy is available on pypi, because PuePy is intended primarily as a client-side framework, \"installation\" is best achieved by downloading the wheel file and including it in your pyscript packages configuration.

    A simple first project (with no web server) would be:

    • index.html (index.html file)
    • pyscript.json (pyscript config file)
    • hello.py (Hello World code)
    • puepy-0.6.2-py3-none-any.whl (PuePy wheel file)

    The runtime file would contain only the files needed to actually execute PuePy code; no tests or other files. Runtime zips are available in each release's notes on GitHub.

    "},{"location":"installation/#downloading-client-runtime","title":"Downloading client runtime","text":"
    curl -O https://download.puepy.dev/puepy-0.6.2-py3-none-any.whl\n
    "},{"location":"installation/#setting-up-your-first-project","title":"Setting up your first project","text":"

    Continue to the tutorial to see how to set up your first project.

    "},{"location":"cookbook/loading-indicators/","title":"Showing Loading Indicators","text":"

    PyScript, on which PuePy is built, provides two runtime options. When combined with PuePy, the total transfer size to render a PuePy page as reported by Chromium's dev tools for each runtime are:

    Runtime Transfer Size MicroPython 353 KB Pyodide 5.9 MB

    MicroPython's runtime, even on a slower connection, is well within the bounds of normal web frameworks. Pyodide, however, will be perceived as initially quite slow to load on slower connections. Pyodide may be workable for internal line-of-business software where users have fast connections or in cases where it's accepted that an application may take some time to initially load, but will be cached during further use.

    "},{"location":"cookbook/loading-indicators/#showing-an-indicator-before-puepy-loads","title":"Showing an indicator before PuePy loads","text":"

    Before you mount your PuePy page into its target element, the target element's HTML is rendered in the browser. A very simple way to show that PuePy hasn't loaded is to include an indicator in the target element, which will be replaced upon execution by PuePy:

    <div id=\"app\">Loading...</div>\n

    The Full App Template example from the tutorial makes use of a Shoelace web component to show a visual loading indicator as a spinning wheel:

    <!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Example</title>\n    <link rel=\"stylesheet\" href=\"app.css\">\n\n    <link rel=\"stylesheet\" href=\"https://pyscript.net/releases/2025.2.2/core.css\">\n    <script type=\"module\" src=\"https://pyscript.net/releases/2025.2.2/core.js\"></script>\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/light.css\"/>\n    <script type=\"module\" src=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace.js\"></script>\n</head>\n<body>\n<!-- Show the application with a loading indicator that will be replaced later -->\n<div id=\"app\">\n    <div style=\"text-align: center; height: 100vh; display: flex; justify-content: center; align-items: center;\">\n        <sl-spinner style=\"font-size: 50px; --track-width: 10px;\"></sl-spinner>\n    </div>\n</div>\n<script type=\"mpy\" src=\"./main.py\" config=\"./pyscript-app.json\"></script>\n</body>\n</html>\n

    This will render as a loading indicator, animated, visible only until PuePy mounts the real application code:

    "},{"location":"cookbook/navigation-guards/","title":"Navigation Guards","text":"

    When a page loads, you can guard navigation to that page by running a precheck \u2013 a method that runs before the page is rendered. If the precheck raises an exception, the page is not rendered, and the exception is caught by the framework. This is useful for checking if a user is authenticated, for example.

    Here's an example of a precheck that raises an exception if the user is not authenticated:

    Showing error
    from puepy import exceptions, Page\n\nclass MyPage(Page):\n    ...\n    def precheck(self):\n        if not self.application.state[\"authenticated_user\"]:\n            raise exceptions.Unauthorized()\n

    In this example, if the authenticated_user key in the application state is False, the page will not render, and an Unauthorized exception will be raised. PuePy will then display your application.unauthorized_page.

    Alternatively, you could redirect the user by raising puepu.exceptions.Redirect:

    Redirecting to a login page
    from puepy import exceptions, Page\n\n\nclass LoginPage(Page):\n    ...\n\n\nclass MyPage(Page):\n    ...\n    def precheck(self):\n        if not self.application.state[\"authenticated_user\"]:\n            raise exceptions.Redirect(LoginPage)\n
    "},{"location":"guide/advanced-routing/","title":"Router","text":"

    Client-side routing in PuePy is optional. If enabled, the router allows you to define multiple \"pages\" with their own URLs.

    See Also

    • Tutorial: Routing
    • Reference: puepy.router

    If you do not install the router, you can only define one page, and that page will be mounted on the target element. If you install the router, the browser's URL will determine which page is mounted, based on the link mode used.

    "},{"location":"guide/advanced-routing/#installing-the-router","title":"Installing the router","text":"

    Routing is an entirely optional feature of PuePy. Many projects may prefer to rely on backend-side routing, like a traditional web project. However if you are developing a single-page app, or simply want to use multiple \"subpages\" on the page you made using PuePy, you must install the router by calling app.install_router.

    from puepy import Application\nfrom puepy.router import Router\n\n\napp = Application()\napp.install_router(Router, link_mode=Router.LINK_MODE_HASH)\n

    link_mode defines how PuePy both creates and parses URLs. There are three options:

    link_mode description pro/con Router.LINK_MODE_HASH Uses window location \"anchor\" (the part of the URL after a #) to determine route Simple implementation, works with any backend Router.LINK_MODE_HTML5 Uses browser's history API to manipulate page URLs without causing a reload Cleaner URLs (but requires backend configuration) Router.LINK_MODE_DIRECT Keeps routing information on the client, but links directly to pages, causing reload Might be ideal for niche use cases or server-side rendering

    Tip

    If you want to use client-side routing but aren't sure what link_mode to enable, \"hash\" is probably your best bet.

    "},{"location":"guide/advanced-routing/#accessing-the-router-object","title":"Accessing the Router object","text":"

    Once enabled, the Router object may be accessed on any component using self.page.router.

    "},{"location":"guide/advanced-routing/#defining-routes","title":"Defining routes","text":"

    The preferred way of adding pages to the router is by calling the @app.page decorator on your Application object (see Hello World in the tutorial.

    from puepy import Page\n\n\n@app.page(\"/my-page\")\nclass MyPage(Page):\n    ...\n

    Or, define a default route by not passing a path.

    @app.page()\nclass MyPage(Page):\n    ...\n

    You can also add routes directly, though this isn't the preferred method.

    app.router.add_route(path_match=\"/foobar\", page_class=Foobar)\n
    "},{"location":"guide/advanced-routing/#route-names","title":"Route names","text":"

    By default, routes are named by converting the class name to lower case, and replacing MixedCase with under_scores. For instance, MyPage would be converted to my_page as a route name. You may, however, give your routes custom names by passing a name parameter to either add_route or @app.page:

    @app.page(\"/my-page\", name=\"another_name\")\nclass MyPage(Page):\n    ...\n\n\nclass AnotherPage(Page):\n    ...\n\n\napp.add_route(\"/foobar\", AnotherPage, name=\"foobar\")\n
    "},{"location":"guide/advanced-routing/#passing-parameters-to-pages","title":"Passing parameters to pages","text":"

    Pages can accept parameters fropm the router with props matching placeholders defined in the route path.

    @app.page(\"/post/<author_id>/<post_id>/view\")\nclass PostPage(Page):\n    props = [\"author_id\", \"post_id\"]\n\n    def populate(self):\n        t.p(f\"This is a post from {self.author_id}->{self.user_id}\")\n
    "},{"location":"guide/advanced-routing/#reversing-routes","title":"Reversing routes","text":"

    Call router.reverse with the page you want to find the route for, along with any relevant arguments.

    path = self.page.router.reverse(PostPage, author_id=\"5\", post_id=\"7\")\n

    The page can either be the Page object itself, or the route name.

    path = self.page.router.reverse(\"post_page\", author_id=\"5\", post_id=\"7\")\n
    "},{"location":"guide/css-classes/","title":"CSS Classes","text":"

    Although you are in charge of your own CSS, PuePy provides some convenience mechanisms for defining CSS classes. Because class is a reserved word in Python, when passing classes to tags, you should use either class_name or classes. Each can be defined as a string, list, or dictionary:

    @app.page()\nclass HelloWorldPage(Page):\n    def populate(self):\n        t.button(\"Primary Large Button\", class_name=\"primary large\")\n        t.button(\"Primary Small Button\", classes=[\"primary\", \"small\"])\n        t.button(\"Primary Medium Button\", classes={\n            \"primary\": True, \n            \"medium\": True, \n            \"small\": False, \n            \"large\": False})\n

    Notice that when passing a dictionary, the value of the dictionary indicates whether the class will be included.

    "},{"location":"guide/css-classes/#components-and-classes","title":"Components and classes","text":"

    Components can define default classes. For example in the Components section of the tutorial, we define a Card component:

    @t.component()\nclass Card(Component):\n    ...\n\n    default_classes = [\"card\"]\n\n    ...\n

    The default_classes attribute tells PuePy to render the component with card as a default class. Code using the Card component can add to or even remove the default classes defined by the component.

    To remove a class, pass it with a \"/\" prefix:

    class MyPage(Page):\n    def populate(self):\n        # This will render as a div with both \"card\" and \"card-blue\" \n        # classes.\n        t.card(classes=\"card-blue\")\n\n        # This will override the default and remove the \"card\" class\n        t.card(classes=\"/card\")        \n
    "},{"location":"guide/in-depth-components/","title":"In-Depth Components","text":"

    Defining components in PuePy is a powerful way to encapsulate data, display, and logic in a reusable way. Components become usable like tags in the populate() method of other components or pages, define slots, props, and events.

    "},{"location":"guide/in-depth-components/#data-flow-features","title":"Data flow features","text":""},{"location":"guide/in-depth-components/#slots","title":"Slots","text":"

    Slots are a mechanism to allow parent components to inject content (tags, other components, etc) into child components in specified locations. There can be one default or unamed slot, and any number of named slots.

    • Slots are defined in the populate() method of a component or page using self.insert_slot.
    • Slots are consumed in the code using the component with a context manager object and <component>.slot().

    See the Components Tutorial Chapter for more information on slots.

    "},{"location":"guide/in-depth-components/#props","title":"Props","text":"

    Props are a way to pass data to child components. Props must be defined by a component. When writing a component, you can simply include props as a list of strings, where each element is the name of a prop, or include instances of the Prop class. You can mix and match the two as well

    class MyComponent(Component):\n    props = [\n        \"title\",  # (1)!\n        Prop(\"author_name\", \"Name of Author\", str, \"Unknown\") # (2)!\n    ]\n
    1. This is a prop which only defines a name.
    2. To add extra metadata about a prop, you can also define a Prop instance.

    Regardless of how you define props in your component, a full \"expanded\" list of props is available on self.props_expanded as a dictionary mapping prop name to Prop instance, with the Prop instance created automatically if only a name is specified.

    See Also

    • Prop Class Reference
    "},{"location":"guide/in-depth-components/#attributes","title":"Attributes","text":"

    Keyword arguments passed to a component that do not match any known prop are considered attributes and stored in self.attrs. They are then inserted into the rendered DOM as HTML elements on the rendered attribute. This means that, for instance, you can pass arbitrary HTML attributes to newly created components without defining any custom props or other logic. Eg,

    from puepy import Component, Page, t\n\n\nclass NameInput(Component):\n    enclosing_tag = \"input\"\n\nclass MyPage(Page):\n    def populate(self):\n        t.name_input(id=\"name_input\", placeholder=\"Enter your name\")\n

    Even if the NameInput component does not define a placeholder prop, the placeholder attribute will be rendered on the input tag.

    When to use props vs attributes?

    • Use props when you want to pass data to a component that will be used in the component's logic or rendering.
    • Use attributes when you want to pass data to a component that will be used in the rendered HTML but not in the component's logic.
    "},{"location":"guide/in-depth-components/#events","title":"Events","text":"

    Events are a way to allow child components to communicate with parent components. When writing a component, in your own code, you can emit an event by calling self.trigger_event. You can also optionally pass a detail dictionary to the event, which will be passed along (after Python to JavaScript conversion) to the browser's native event system in JavaScript.

    For example, suppose you want to emit a custom event, greeting, with a type attribute:

    class MyComponent(Component):\n    def some_method(self):\n        self.trigger_event(\"greeting\", detail={\"message\": \"Hello There\"})\n

    A consumer of your component can listen for this event by defining an on_greeting method in their component or page:

    class MyPage(Page):\n    def populate(self):\n        t.my_component(on_greeting=self.on_greeting_sent)\n\n    def on_greeting_sent(self, event):\n        print(\"Incoming message from component\", event.detail.get('message'))\n

    See Also

    Mozilla's guide to JavaScript events

    "},{"location":"guide/in-depth-components/#customization","title":"Customization","text":"

    You have several ways of controlling how your components are rendered. First, you can define what enclosing tag your component is rendered as. The default is a div tag, but this can be overridden:

    class MyInputComponent(Component):\n    enclosing_tag = \"input\"\n

    You can also define default classes, default attributes, and the default role for your component:

    class MyInputComponent(Component):\n    enclosing_tag = \"input\"\n\n    default_classes = [\"my-input\"]\n    default_attributes = {\"type\": \"text\"}\n    default_role = \"textbox\"\n
    "},{"location":"guide/in-depth-components/#parentchild-relationships","title":"Parent/Child relationships","text":"

    Each tag (and thus each component) in PuePy has a parent unless it is the root page. Consider the following example:

    from puepy import Application, Page, Component, t\n\napp = Application()\n\n\n@t.component()\nclass CustomInput(Component):\n    enclosing_tag = \"input\"\n\n    def on_event_handle(self, event):\n        print(self.parent)\n\n\nclass MyPage(Page):\n    def populate(self):\n        with t.div():\n            t.custom_input()\n

    In this example, the parent of the CustomInput instance is not the MyPage instance, it is the div, a puepy.Tag instance. In many cases, you will want to interact another relevant object, not necessarily the one immediately parental of your current instance. In those instances, from your components, you may reference:

    • self.page (Page instance): The page ultimately responsible for rendering this component
    • self.origin (Component or Page instance): The component that created yours in its populate() method
    • self.parent (Tag instance): The direct parent of the current instance

    Additionally, parent instances have the following available:

    • self.children (list): Direct child nodes
    • self.refs (dict): Instances created during this instance's most recent populate() method

    Warning

    None of the attributes regarding parent/child/origin relationships should be modified by application code. Doing so could result in unexpected behavior.

    "},{"location":"guide/in-depth-components/#refs","title":"Refs","text":"

    In addition to parent/child relationships, most components and pages define an entire hierarchy of tags and components in the populate() method. If you want to reference components later, or tell PuePy which component is which (in case the ordering changes in sebsequent redraws), using a ref= argument when building tags:

    class MyPage(Page):\n    def populate(self):\n        t.button(\"My Button\", ref=\"my_button\")\n\n    def auto_click_button(self, ...):\n        self.refs[\"my_button\"].element.click()\n

    For more information on why this is useful, see the Refs Tutorial Topic.

    "},{"location":"guide/pyscript-config/","title":"PyScript Config","text":"

    PyScript's configuration is fully documented in the PyScript documentation. Configuration for PuePy simply requires adding the PuePy runtime files (see Quick Start - Installation) and Morphdom:

    {\n  \"name\": \"PuePy Tutorial\",\n  \"debug\": true,\n  \"packages\": [\n    \"./puepy-0.6.2-py3-none-any.whl\"\n  ],\n  \"js_modules\": {\n    \"main\": {\n      \"https://cdn.jsdelivr.net/npm/morphdom@2.7.2/+esm\": \"morphdom\"\n    }\n  }\n}\n
    "},{"location":"guide/reactivity/","title":"Reactivity","text":"

    Reactivity is a paradigm that causes the user interface to update automatically in response to changes in the application state. Rather than triggering updates manually as a programmer, you can be assured that the application state will trigger redraws, with new information, as needed. Reactivity in PuePy is inspired by Vue.js.

    "},{"location":"guide/reactivity/#state","title":"State","text":""},{"location":"guide/reactivity/#initial-state","title":"Initial state","text":"

    Components (including Pages) define initial state through the initial() method:

    class MyComponent(Component):\n    def initial(self):\n        return {\n            \"name\": \"Monty ... Something?\",\n            \"movies\": [\"Monty Python and the Holy Grail\"]\n        }\n
    "},{"location":"guide/reactivity/#modifying-state","title":"Modifying state","text":"

    If any method on the component changed the name, it would trigger a UI refresh:

    class MyComponent(Component):\n    def update_name(self):\n        # This triggers a refresh\n        self.state[\"name\"] = \"Monty Python\"\n
    "},{"location":"guide/reactivity/#modifying-mutable-objects-in-place","title":"Modifying mutable objects in-place","text":"

    Warnign

    PuePy's reactivity works by using dictionary __setitem__ and __delitem__ methods. As such, it cannot detect \"nested\" updates or changes to mutable objects in the state. If your code will result in a state change such as a data structure being changed in-place, you must a mutate() context manager.

    Modifying complex (mutable) data structures in place without setting them will not work:

    class MyComponent(Component):\n    def update_movies(self):\n        # THIS WILL NOT CAUSE A UI REFRESH!\n        self.state[\"movies\"].append(\"Monty Python\u2019s Life of Brian\")\n

    Instead, use a context manager to tell the state object what is being modified. This is ideal anyway.

    class MyComponent(Component):\n    def update_movies(self):\n        # THIS WILL NOT CAUSE A UI REFRESH!\n        with self.state.mutate(\"movies\"):\n            self.state[\"movies\"].append(\"Monty Python\u2019s Life of Brian\")\n

    mutate(*keys) can be called with any number of keys you intend to modify. As an added benefit, the state change will only call listeners after the context manager exits, making it ideal also for \"batching up\" changes.

    "},{"location":"guide/reactivity/#controlling-ui-refresh","title":"Controlling UI Refresh","text":""},{"location":"guide/reactivity/#disabling-automatic-refresh","title":"Disabling Automatic Refresh","text":"

    By default, any detected mutations to a component's state will trigger a UI fresh. This can be customized. To disable automatic refresh entirely, set redraw_on_changes to False.

    class MyComponent(Component):\n    # The UI will no longer refresh on state changes\n    redraw_on_changes = False\n\n    def something_happened(self):\n        # This can be called to manually refresh this component and its children\n        self.trigger_redraw()\n\n        # Or, you can redraw the whole page\n        self.page.trigger_redraw()\n
    "},{"location":"guide/reactivity/#limiting-automatic-refresh","title":"Limiting Automatic Refresh","text":"

    Suppose that you want to refresh the UI on some state changes, but not others.

    class MyComponent(Component):\n    # When items in this this change, the UI will be redrawn\n    redraw_on_changes = [\"items\"]\n
    "},{"location":"guide/reactivity/#watching-for-changes","title":"Watching for changes","text":"

    You can watch for changes in state yourself.

    class MyComponent(Component):\n    def initial():\n        return {\"spam\": \"eggs\"}\n\n    def on_spam_change(self, new_value):\n        print(\"New value for spam\", new_value)\n

    Or, watch for any state change:

    class MyComponent(Component):\n    def on_state_change(self, key, value):\n        print(key, \"was set to\", value)\n
    "},{"location":"guide/reactivity/#binding-form-element-values-to-state","title":"Binding form element values to state","text":"

    For your convenience, the bind parameter can be used to automatically establish a two-way connection between input elements and component state. When the value of a form element changes, the state is updated. When the state is updated, the corresponding form tag's value reflects that change.

    class MyComponent(Component):\n    def initial(self):\n        return {\"name\": \"\"}\n\n    def populate(self):\n        # bind specifies what key on self.state should be tied to this input's value\n        t.input(placeholder=\"Type your name\", bind=\"name\")\n
    "},{"location":"guide/reactivity/#application-state","title":"Application State","text":"

    In addition to components and pages, there is also a \"global\" application-wide state. Note that this state is only for a running Application instance and does not survive reloads nor is it shared across multiple browser tabs or windows.

    To use the application state, use application.state as you would local state. For example, in the Full App Template tutorial chapter, the working example uses self.application.state[\"authenticated_user\"] in a variety of places:

    Navigation Guard
    def precheck(self):\n    if not self.application.state[\"authenticated_user\"]:\n        raise exceptions.Unauthorized()\n
    Setting state
    self.application.state[\"authenticated_user\"] = self.state[\"username\"]\n
    Rendering based on application state
    def populate(self):\n    ...\n\n    t.h1(f\"Hello, you are authenticated as {self.application.state['authenticated_user']}\")\n

    As with page or component state, changes to the application state trigger refreshes by default. That behavior can be controlled with redraw_on_app_state_changes on components or pages:

    class Page1(Page):\n    redraw_on_app_state_changes = True  # (1)!\n\n\nclass Page2(Page):\n    redraw_on_app_state_changes = False  # (2)!\n\n\nclass Page3(Page):\n    redraw_on_app_state_changes = [\"authenticated_user\"]  # (3)!\n
    1. The default behavior, with redraw_on_app_state_changes set to True, all changes to application state trigger a redraw.
    2. Setting redraw_on_app_state_changes to False prevents changes to application state from triggering a redraw.
    3. Setting redraw_on_app_state_changes to a list of keys will trigger a redraw only when those keys change.

    Tip

    This behavior mirrors redraw_on_state_changes, which is used for local state.

    "},{"location":"guide/runtimes/","title":"Runtimes","text":"

    From its upstream project, PyScript, PuePy supports two runtime environments:

    • MicroPython
    • Pyodide

    There are some interface differences, as well as technical ones, described in the official PyScript docs. Additionally, many standard library features are missing from MicroPython. MicroPython does not have access to PyPi packages, nor does MicroPython type hinting or other advanced features as thoroughly as Pyodide.

    MicroPython, however, has just a ~170k runtime, making it small enough to load on \"normal\" websites without the performance hit of Pyodide's 11MB runtime. It is ideal for situations where webpage response time is important.

    "},{"location":"guide/runtimes/#when-to-use-pyodide","title":"When to use Pyodide","text":"

    You may consider using Pyodide when:

    • Initial load time is less important
    • You need to use PyPi packages
    • You need to use advanced Python features
    • You need to use the full Python standard library
    • You need to use type hinting
    • You need to use Python 3.9 or later
    "},{"location":"guide/runtimes/#when-to-use-micropython","title":"When to use MicroPython","text":"

    You may consider using MicroPython when:

    • Initial load time is important
    • Your PuePy code will use only simple Python features to add reactivity and interactivity to websites
    "},{"location":"guide/runtimes/#how-to-switch-runtimes","title":"How to switch runtimes","text":"

    To choose a runtime, specify either type=\"mpy\" or type=\"py\" in your <script> tag when loading PuePy. For example:

    "},{"location":"guide/runtimes/#loading-pyodide","title":"Loading Pyodide","text":"
    <script type=\"mpy\" src=\"./main.py\" config=\"pyscript.json\"></script>\n
    "},{"location":"guide/runtimes/#loading-micropython","title":"Loading MicroPython","text":"
    <script type=\"mpy\" src=\"./main.py\" config=\"pyscript.json\"></script>\n

    See Also

    • PyScript Architecture: Interpreters
    • Pyodide Project
    • MicroPython Project
    "},{"location":"reference/application/","title":"puepy.Application","text":"

    The puepy.Application class is a core part of PuePy. It is the main entry point for creating PuePy applications. The Application class is used to manage the application's state, components, and pages.

    Bases: Stateful

    The main application class for PuePy. It manages the state, storage, router, and pages for the application.

    Attributes:

    Name Type Description state ReactiveDict

    The state object for the application.

    session_storage BrowserStorage

    The session storage object for the application.

    local_storage BrowserStorage

    The local storage object for the application.

    router Router

    The router object for the application, if any

    default_page Page

    The default page to mount if no route is matched.

    active_page Page

    The currently active page.

    not_found_page Page

    The page to mount when a 404 error occurs.

    forbidden_page Page

    The page to mount when a 403 error occurs.

    unauthorized_page Page

    The page to mount when a 401 error occurs.

    error_page Page

    The page to mount when an error occurs.

    Source code in puepy/application.py
    class Application(Stateful):\n    \"\"\"\n    The main application class for PuePy. It manages the state, storage, router, and pages for the application.\n\n    Attributes:\n        state (ReactiveDict): The state object for the application.\n        session_storage (BrowserStorage): The session storage object for the application.\n        local_storage (BrowserStorage): The local storage object for the application.\n        router (Router): The router object for the application, if any\n        default_page (Page): The default page to mount if no route is matched.\n        active_page (Page): The currently active page.\n        not_found_page (Page): The page to mount when a 404 error occurs.\n        forbidden_page (Page): The page to mount when a 403 error occurs.\n        unauthorized_page (Page): The page to mount when a 401 error occurs.\n        error_page (Page): The page to mount when an error occurs.\n    \"\"\"\n\n    def __init__(self, element_id_generator=None):\n        self.state = ReactiveDict(self.initial())\n        self.add_context(\"state\", self.state)\n\n        if is_server_side:\n            self.session_storage = None\n            self.local_storage = None\n        else:\n            from js import localStorage, sessionStorage\n\n            self.session_storage = BrowserStorage(sessionStorage, \"session_storage\")\n            self.local_storage = BrowserStorage(localStorage, \"local_storage\")\n        self.router = None\n        self._selector_or_element = None\n        self.default_page = None\n        self.active_page = None\n\n        self.not_found_page = GenericErrorPage\n        self.forbidden_page = GenericErrorPage\n        self.unauthorized_page = GenericErrorPage\n        self.error_page = TracebackErrorPage\n\n        self.element_id_generator = element_id_generator or DefaultIdGenerator()\n\n    def install_router(self, router_class, **kwargs):\n        \"\"\"\n        Install a router in the application.\n\n        Args:\n            router_class (class): A class that implements the router logic for the application. At this time, only\n                `puepy.router.Router` is available.\n            **kwargs: Additional keyword arguments that can be passed to the router_class constructor.\n        \"\"\"\n        self.router = router_class(application=self, **kwargs)\n        if not is_server_side:\n            add_event_listener(window, \"popstate\", self._on_popstate)\n\n    def page(self, route=None, name=None):\n        \"\"\"\n        A decorator for `Page` classes which adds the page to the application with a specified route and name.\n\n        Intended to be called as a decorator.\n\n        Args:\n            route (str): The route for the page. Default is None.\n            name (str): The name of the page. If left None, page class is used as the name.\n\n        Examples:\n            ``` py\n            app = Application()\n            @app.page(\"/my-page\")\n            class MyPage(Page):\n                ...\n            ```\n        \"\"\"\n        if route:\n            if not self.router:\n                raise Exception(\"Router not installed\")\n\n            def decorator(func):\n                self.router.add_route(route, func, name=name)\n                return func\n\n            return decorator\n        else:\n\n            def decorator(func):\n                self.default_page = func\n                return func\n\n            return decorator\n\n    def _on_popstate(self, event):\n        if self.router.link_mode == self.router.LINK_MODE_HASH:\n            self.mount(self._selector_or_element, window.location.hash.split(\"#\", 1)[-1])\n        elif self.router.link_mode in (self.router.LINK_MODE_DIRECT, self.router.LINK_MODE_HTML5):\n            self.mount(self._selector_or_element, window.location.pathname)\n\n    def remount(self, path=None, page_kwargs=None):\n        \"\"\"\n        Remounts the selected element or selector with the specified path and page_kwargs.\n\n        Args:\n            path (str): The new path to be used for remounting the element or selector. Default is None.\n            page_kwargs (dict): Additional page kwargs to be passed when remounting. Default is None.\n\n        \"\"\"\n        self.mount(self._selector_or_element, path=path, page_kwargs=page_kwargs)\n\n    def mount(self, selector_or_element, path=None, page_kwargs=None):\n        \"\"\"\n        Mounts a page onto the specified selector or element with optional path and page_kwargs.\n\n        Args:\n            selector_or_element: The selector or element on which to mount the page.\n            path: Optional path to match against the router. Defaults to None.\n            page_kwargs: Optional keyword arguments to pass to the mounted page. Defaults to None.\n\n        Returns:\n            (Page): The mounted page instance\n        \"\"\"\n        if page_kwargs is None:\n            page_kwargs = {}\n\n        self._selector_or_element = selector_or_element\n\n        if self.router:\n            path = path or self.current_path\n            route, arguments = self.router.match(path)\n            if arguments:\n                page_kwargs.update(arguments)\n\n            if route:\n                page_class = route.page\n            elif path in (\"\", \"/\") and self.default_page:\n                page_class = self.default_page\n            elif self.not_found_page:\n                page_class = self.not_found_page\n            else:\n                return None\n        elif self.default_page:\n            route = None\n            page_class = self.default_page\n        else:\n            return None\n\n        self.active_page = None\n        try:\n            self.mount_page(\n                selector_or_element=selector_or_element,\n                page_class=page_class,\n                route=route,\n                page_kwargs=page_kwargs,\n                handle_exceptions=True,\n            )\n        except Exception as e:\n            self.handle_error(e)\n        return self.active_page\n\n    @property\n    def current_path(self):\n        \"\"\"\n        Returns the current path based on the router's link mode.\n\n        Returns:\n            str: The current path.\n        \"\"\"\n        if self.router.link_mode == self.router.LINK_MODE_HASH:\n            return window.location.hash.split(\"#\", 1)[-1]\n        elif self.router.link_mode in (self.router.LINK_MODE_DIRECT, self.router.LINK_MODE_HTML5):\n            return window.location.pathname\n        else:\n            return \"\"\n\n    def mount_page(self, selector_or_element, page_class, route, page_kwargs, handle_exceptions=True):\n        \"\"\"\n        Mounts a page on the specified selector or element with the given parameters.\n\n        Args:\n            selector_or_element (str or Element): The selector string or element to mount the page on.\n            page_class (class): The page class to mount.\n            route (str): The route for the page.\n            page_kwargs (dict): Additional keyword arguments to pass to the page class.\n            handle_exceptions (bool, optional): Determines whether to handle exceptions thrown during mounting.\n                Defaults to True.\n        \"\"\"\n        page_class._expanded_props()\n\n        # For security, we only pass props to the page that are defined in the page's props\n        #\n        # We also handle the list or not-list props for multiple or single values\n        # (eg, ?foo=1&foo=2 -> [\"1\", \"2\"] if needed)\n        #\n        prop_args = {}\n        prop: Prop\n        for prop in page_class.props_expanded.values():\n            if prop.name in page_kwargs:\n                value = page_kwargs.pop(prop.name)\n                if prop.type is list:\n                    prop_args[prop.name] = value if isinstance(value, list) else [value]\n                else:\n                    prop_args[prop.name] = value if not isinstance(value, list) else value[0]\n\n        self.active_page: Page = page_class(matched_route=route, application=self, extra_args=page_kwargs, **prop_args)\n        try:\n            self.active_page.mount(selector_or_element)\n        except exceptions.PageError as e:\n            if handle_exceptions:\n                self.handle_page_error(e)\n            else:\n                raise\n\n    def handle_page_error(self, exc):\n        \"\"\"\n        Handles page error based on the given exception by inspecting the exception type and passing it along to one\n        of:\n\n        - `handle_not_found`\n        - `handle_forbidden`\n        - `handle_unauthorized`\n        - `handle_redirect`\n        - `handle_error`\n\n        Args:\n            exc (Exception): The exception object representing the page error.\n        \"\"\"\n        if isinstance(exc, exceptions.NotFound):\n            self.handle_not_found(exc)\n        elif isinstance(exc, exceptions.Forbidden):\n            self.handle_forbidden(exc)\n        elif isinstance(exc, exceptions.Unauthorized):\n            self.handle_unauthorized(exc)\n        elif isinstance(exc, exceptions.Redirect):\n            self.handle_redirect(exc)\n        else:\n            self.handle_error(exc)\n\n    def handle_not_found(self, exception):\n        \"\"\"\n        Handles the exception for not found page. By default, it mounts the self.not_found_page class and passes it\n        the exception as an argument.\n\n        Args:\n            exception (Exception): The exception that occurred.\n        \"\"\"\n        self.mount_page(\n            self._selector_or_element, self.not_found_page, None, {\"error\": exception}, handle_exceptions=False\n        )\n\n    def handle_forbidden(self, exception):\n        \"\"\"\n        Handles the exception for forbidden page. By default, it mounts the self.forbidden_page class and passes it\n        the exception as an argument.\n\n        Args:\n            exception (Exception): The exception that occurred.\n        \"\"\"\n        self.mount_page(\n            self._selector_or_element,\n            self.forbidden_page,\n            None,\n            {\"error\": exception},\n            handle_exceptions=False,\n        )\n\n    def handle_unauthorized(self, exception):\n        \"\"\"\n        Handles the exception for unauthorized page. By default, it mounts the self.unauthorized_page class and passes it\n        the exception as an argument.\n\n        Args:\n            exception (Exception): The exception that occurred.\n        \"\"\"\n        self.mount_page(\n            self._selector_or_element, self.unauthorized_page, None, {\"error\": exception}, handle_exceptions=False\n        )\n\n    def handle_error(self, exception):\n        \"\"\"\n        Handles the exception for application or unknown errors. By default, it mounts the self.error_page class and\n        passes it the exception as an argument.\n\n        Args:\n            exception (Exception): The exception that occurred.\n        \"\"\"\n        self.mount_page(self._selector_or_element, self.error_page, None, {\"error\": exception}, handle_exceptions=False)\n        if is_server_side:\n            raise\n\n    def handle_redirect(self, exception):\n        \"\"\"\n        Handles a redirect exception by navigating to the given path.\n\n        Args:\n            exception (RedirectException): The redirect exception containing the path to navigate to.\n        \"\"\"\n        self.router.navigate_to_path(exception.path)\n
    "},{"location":"reference/application/#puepy.Application.current_path","title":"current_path property","text":"

    Returns the current path based on the router's link mode.

    Returns:

    Name Type Description str

    The current path.

    "},{"location":"reference/application/#puepy.Application.handle_error","title":"handle_error(exception)","text":"

    Handles the exception for application or unknown errors. By default, it mounts the self.error_page class and passes it the exception as an argument.

    Parameters:

    Name Type Description Default exception Exception

    The exception that occurred.

    required Source code in puepy/application.py
    def handle_error(self, exception):\n    \"\"\"\n    Handles the exception for application or unknown errors. By default, it mounts the self.error_page class and\n    passes it the exception as an argument.\n\n    Args:\n        exception (Exception): The exception that occurred.\n    \"\"\"\n    self.mount_page(self._selector_or_element, self.error_page, None, {\"error\": exception}, handle_exceptions=False)\n    if is_server_side:\n        raise\n
    "},{"location":"reference/application/#puepy.Application.handle_forbidden","title":"handle_forbidden(exception)","text":"

    Handles the exception for forbidden page. By default, it mounts the self.forbidden_page class and passes it the exception as an argument.

    Parameters:

    Name Type Description Default exception Exception

    The exception that occurred.

    required Source code in puepy/application.py
    def handle_forbidden(self, exception):\n    \"\"\"\n    Handles the exception for forbidden page. By default, it mounts the self.forbidden_page class and passes it\n    the exception as an argument.\n\n    Args:\n        exception (Exception): The exception that occurred.\n    \"\"\"\n    self.mount_page(\n        self._selector_or_element,\n        self.forbidden_page,\n        None,\n        {\"error\": exception},\n        handle_exceptions=False,\n    )\n
    "},{"location":"reference/application/#puepy.Application.handle_not_found","title":"handle_not_found(exception)","text":"

    Handles the exception for not found page. By default, it mounts the self.not_found_page class and passes it the exception as an argument.

    Parameters:

    Name Type Description Default exception Exception

    The exception that occurred.

    required Source code in puepy/application.py
    def handle_not_found(self, exception):\n    \"\"\"\n    Handles the exception for not found page. By default, it mounts the self.not_found_page class and passes it\n    the exception as an argument.\n\n    Args:\n        exception (Exception): The exception that occurred.\n    \"\"\"\n    self.mount_page(\n        self._selector_or_element, self.not_found_page, None, {\"error\": exception}, handle_exceptions=False\n    )\n
    "},{"location":"reference/application/#puepy.Application.handle_page_error","title":"handle_page_error(exc)","text":"

    Handles page error based on the given exception by inspecting the exception type and passing it along to one of:

    • handle_not_found
    • handle_forbidden
    • handle_unauthorized
    • handle_redirect
    • handle_error

    Parameters:

    Name Type Description Default exc Exception

    The exception object representing the page error.

    required Source code in puepy/application.py
    def handle_page_error(self, exc):\n    \"\"\"\n    Handles page error based on the given exception by inspecting the exception type and passing it along to one\n    of:\n\n    - `handle_not_found`\n    - `handle_forbidden`\n    - `handle_unauthorized`\n    - `handle_redirect`\n    - `handle_error`\n\n    Args:\n        exc (Exception): The exception object representing the page error.\n    \"\"\"\n    if isinstance(exc, exceptions.NotFound):\n        self.handle_not_found(exc)\n    elif isinstance(exc, exceptions.Forbidden):\n        self.handle_forbidden(exc)\n    elif isinstance(exc, exceptions.Unauthorized):\n        self.handle_unauthorized(exc)\n    elif isinstance(exc, exceptions.Redirect):\n        self.handle_redirect(exc)\n    else:\n        self.handle_error(exc)\n
    "},{"location":"reference/application/#puepy.Application.handle_redirect","title":"handle_redirect(exception)","text":"

    Handles a redirect exception by navigating to the given path.

    Parameters:

    Name Type Description Default exception RedirectException

    The redirect exception containing the path to navigate to.

    required Source code in puepy/application.py
    def handle_redirect(self, exception):\n    \"\"\"\n    Handles a redirect exception by navigating to the given path.\n\n    Args:\n        exception (RedirectException): The redirect exception containing the path to navigate to.\n    \"\"\"\n    self.router.navigate_to_path(exception.path)\n
    "},{"location":"reference/application/#puepy.Application.handle_unauthorized","title":"handle_unauthorized(exception)","text":"

    Handles the exception for unauthorized page. By default, it mounts the self.unauthorized_page class and passes it the exception as an argument.

    Parameters:

    Name Type Description Default exception Exception

    The exception that occurred.

    required Source code in puepy/application.py
    def handle_unauthorized(self, exception):\n    \"\"\"\n    Handles the exception for unauthorized page. By default, it mounts the self.unauthorized_page class and passes it\n    the exception as an argument.\n\n    Args:\n        exception (Exception): The exception that occurred.\n    \"\"\"\n    self.mount_page(\n        self._selector_or_element, self.unauthorized_page, None, {\"error\": exception}, handle_exceptions=False\n    )\n
    "},{"location":"reference/application/#puepy.Application.install_router","title":"install_router(router_class, **kwargs)","text":"

    Install a router in the application.

    Parameters:

    Name Type Description Default router_class class

    A class that implements the router logic for the application. At this time, only puepy.router.Router is available.

    required **kwargs

    Additional keyword arguments that can be passed to the router_class constructor.

    {} Source code in puepy/application.py
    def install_router(self, router_class, **kwargs):\n    \"\"\"\n    Install a router in the application.\n\n    Args:\n        router_class (class): A class that implements the router logic for the application. At this time, only\n            `puepy.router.Router` is available.\n        **kwargs: Additional keyword arguments that can be passed to the router_class constructor.\n    \"\"\"\n    self.router = router_class(application=self, **kwargs)\n    if not is_server_side:\n        add_event_listener(window, \"popstate\", self._on_popstate)\n
    "},{"location":"reference/application/#puepy.Application.mount","title":"mount(selector_or_element, path=None, page_kwargs=None)","text":"

    Mounts a page onto the specified selector or element with optional path and page_kwargs.

    Parameters:

    Name Type Description Default selector_or_element

    The selector or element on which to mount the page.

    required path

    Optional path to match against the router. Defaults to None.

    None page_kwargs

    Optional keyword arguments to pass to the mounted page. Defaults to None.

    None

    Returns:

    Type Description Page

    The mounted page instance

    Source code in puepy/application.py
    def mount(self, selector_or_element, path=None, page_kwargs=None):\n    \"\"\"\n    Mounts a page onto the specified selector or element with optional path and page_kwargs.\n\n    Args:\n        selector_or_element: The selector or element on which to mount the page.\n        path: Optional path to match against the router. Defaults to None.\n        page_kwargs: Optional keyword arguments to pass to the mounted page. Defaults to None.\n\n    Returns:\n        (Page): The mounted page instance\n    \"\"\"\n    if page_kwargs is None:\n        page_kwargs = {}\n\n    self._selector_or_element = selector_or_element\n\n    if self.router:\n        path = path or self.current_path\n        route, arguments = self.router.match(path)\n        if arguments:\n            page_kwargs.update(arguments)\n\n        if route:\n            page_class = route.page\n        elif path in (\"\", \"/\") and self.default_page:\n            page_class = self.default_page\n        elif self.not_found_page:\n            page_class = self.not_found_page\n        else:\n            return None\n    elif self.default_page:\n        route = None\n        page_class = self.default_page\n    else:\n        return None\n\n    self.active_page = None\n    try:\n        self.mount_page(\n            selector_or_element=selector_or_element,\n            page_class=page_class,\n            route=route,\n            page_kwargs=page_kwargs,\n            handle_exceptions=True,\n        )\n    except Exception as e:\n        self.handle_error(e)\n    return self.active_page\n
    "},{"location":"reference/application/#puepy.Application.mount_page","title":"mount_page(selector_or_element, page_class, route, page_kwargs, handle_exceptions=True)","text":"

    Mounts a page on the specified selector or element with the given parameters.

    Parameters:

    Name Type Description Default selector_or_element str or Element

    The selector string or element to mount the page on.

    required page_class class

    The page class to mount.

    required route str

    The route for the page.

    required page_kwargs dict

    Additional keyword arguments to pass to the page class.

    required handle_exceptions bool

    Determines whether to handle exceptions thrown during mounting. Defaults to True.

    True Source code in puepy/application.py
    def mount_page(self, selector_or_element, page_class, route, page_kwargs, handle_exceptions=True):\n    \"\"\"\n    Mounts a page on the specified selector or element with the given parameters.\n\n    Args:\n        selector_or_element (str or Element): The selector string or element to mount the page on.\n        page_class (class): The page class to mount.\n        route (str): The route for the page.\n        page_kwargs (dict): Additional keyword arguments to pass to the page class.\n        handle_exceptions (bool, optional): Determines whether to handle exceptions thrown during mounting.\n            Defaults to True.\n    \"\"\"\n    page_class._expanded_props()\n\n    # For security, we only pass props to the page that are defined in the page's props\n    #\n    # We also handle the list or not-list props for multiple or single values\n    # (eg, ?foo=1&foo=2 -> [\"1\", \"2\"] if needed)\n    #\n    prop_args = {}\n    prop: Prop\n    for prop in page_class.props_expanded.values():\n        if prop.name in page_kwargs:\n            value = page_kwargs.pop(prop.name)\n            if prop.type is list:\n                prop_args[prop.name] = value if isinstance(value, list) else [value]\n            else:\n                prop_args[prop.name] = value if not isinstance(value, list) else value[0]\n\n    self.active_page: Page = page_class(matched_route=route, application=self, extra_args=page_kwargs, **prop_args)\n    try:\n        self.active_page.mount(selector_or_element)\n    except exceptions.PageError as e:\n        if handle_exceptions:\n            self.handle_page_error(e)\n        else:\n            raise\n
    "},{"location":"reference/application/#puepy.Application.page","title":"page(route=None, name=None)","text":"

    A decorator for Page classes which adds the page to the application with a specified route and name.

    Intended to be called as a decorator.

    Parameters:

    Name Type Description Default route str

    The route for the page. Default is None.

    None name str

    The name of the page. If left None, page class is used as the name.

    None

    Examples:

    app = Application()\n@app.page(\"/my-page\")\nclass MyPage(Page):\n    ...\n
    Source code in puepy/application.py
    def page(self, route=None, name=None):\n    \"\"\"\n    A decorator for `Page` classes which adds the page to the application with a specified route and name.\n\n    Intended to be called as a decorator.\n\n    Args:\n        route (str): The route for the page. Default is None.\n        name (str): The name of the page. If left None, page class is used as the name.\n\n    Examples:\n        ``` py\n        app = Application()\n        @app.page(\"/my-page\")\n        class MyPage(Page):\n            ...\n        ```\n    \"\"\"\n    if route:\n        if not self.router:\n            raise Exception(\"Router not installed\")\n\n        def decorator(func):\n            self.router.add_route(route, func, name=name)\n            return func\n\n        return decorator\n    else:\n\n        def decorator(func):\n            self.default_page = func\n            return func\n\n        return decorator\n
    "},{"location":"reference/application/#puepy.Application.remount","title":"remount(path=None, page_kwargs=None)","text":"

    Remounts the selected element or selector with the specified path and page_kwargs.

    Parameters:

    Name Type Description Default path str

    The new path to be used for remounting the element or selector. Default is None.

    None page_kwargs dict

    Additional page kwargs to be passed when remounting. Default is None.

    None Source code in puepy/application.py
    def remount(self, path=None, page_kwargs=None):\n    \"\"\"\n    Remounts the selected element or selector with the specified path and page_kwargs.\n\n    Args:\n        path (str): The new path to be used for remounting the element or selector. Default is None.\n        page_kwargs (dict): Additional page kwargs to be passed when remounting. Default is None.\n\n    \"\"\"\n    self.mount(self._selector_or_element, path=path, page_kwargs=page_kwargs)\n
    "},{"location":"reference/component/","title":"puepy.Component","text":"

    Components should not be created directly

    In your populate() method, call t.tag_name() to create a component. There's no reason an application develop should directly instanciate a component instance and doing so is not supported.

    See also

    • Tutorial on Components
    • In-Depth Components Guide

    Bases: Tag, Stateful

    Components are a way of defining reusable and composable elements in PuePy. They are a subclass of Tag, but provide additional features such as state management and props. By defining your own components and registering them, you can create a library of reusable elements for your application.

    Attributes:

    Name Type Description enclosing_tag str

    The tag name that will enclose the component. To be defined as a class attribute on subclasses.

    component_name str

    The name of the component. If left blank, class name is used. To be defined as a class attribute on subclasses.

    redraw_on_state_changes bool

    Whether the component should redraw when its state changes. To be defined as a class attribute on subclasses.

    redraw_on_app_state_changes bool

    Whether the component should redraw when the application state changes. To be defined as a class attribute on subclasses.

    props list

    A list of props for the component. To be defined as a class attribute on subclasses.

    Source code in puepy/core.py
    class Component(Tag, Stateful):\n    \"\"\"\n    Components are a way of defining reusable and composable elements in PuePy. They are a subclass of Tag, but provide\n    additional features such as state management and props. By defining your own components and registering them, you\n    can create a library of reusable elements for your application.\n\n    Attributes:\n        enclosing_tag (str): The tag name that will enclose the component. To be defined as a class attribute on subclasses.\n        component_name (str): The name of the component. If left blank, class name is used. To be defined as a class attribute on subclasses.\n        redraw_on_state_changes (bool): Whether the component should redraw when its state changes. To be defined as a class attribute on subclasses.\n        redraw_on_app_state_changes (bool): Whether the component should redraw when the application state changes. To be defined as a class attribute on subclasses.\n        props (list): A list of props for the component. To be defined as a class attribute on subclasses.\n    \"\"\"\n\n    enclosing_tag = \"div\"\n    component_name = None\n    redraw_on_state_changes = True\n    redraw_on_app_state_changes = True\n\n    props = []\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, tag_name=self.enclosing_tag, **kwargs)\n        self.state = ReactiveDict(self.initial())\n        self.add_context(\"state\", self.state)\n\n        self.slots = {}\n\n    def _handle_attrs(self, kwargs):\n        self._handle_props(kwargs)\n\n        super()._handle_attrs(kwargs)\n\n    def _handle_props(self, kwargs):\n        if not hasattr(self, \"props_expanded\"):\n            self._expanded_props()\n\n        self.props_values = {}\n        for name, prop in self.props_expanded.items():\n            value = kwargs.pop(prop.name, prop.default_value)\n            setattr(self, name, value)\n            self.props_values[name] = value\n\n    @classmethod\n    def _expanded_props(cls):\n        # This would be ideal for metaprogramming, but we do it this way to be compatible with Micropython. :/\n        props_expanded = {}\n        for prop in cls.props:\n            if isinstance(prop, Prop):\n                props_expanded[prop.name] = prop\n            elif isinstance(prop, dict):\n                props_expanded[prop[\"name\"]] = Prop(**prop)\n            elif isinstance(prop, str):\n                props_expanded[prop] = Prop(name=prop)\n            else:\n                raise PropsError(f\"Unknown prop type {type(prop)}\")\n        cls.props_expanded = props_expanded\n\n    def initial(self):\n        \"\"\"\n        To be overridden in subclasses, the `initial()` method defines the initial state of the component.\n\n        Returns:\n            (dict): Initial component state\n        \"\"\"\n        return {}\n\n    def _on_state_change(self, context, key, value):\n        super()._on_state_change(context, key, value)\n\n        if context == \"state\":\n            redraw_rule = self.redraw_on_state_changes\n        elif context == \"app\":\n            redraw_rule = self.redraw_on_app_state_changes\n        else:\n            return\n\n        if redraw_rule is True:\n            self.page.redraw_tag(self)\n        elif redraw_rule is False:\n            pass\n        elif isinstance(redraw_rule, (list, set)):\n            if key in redraw_rule:\n                self.page.redraw_tag(self)\n        else:\n            raise Exception(f\"Unknown value for redraw rule: {redraw_rule} (context: {context})\")\n\n    def insert_slot(self, name=\"default\", **kwargs):\n        \"\"\"\n        In defining your own component, when you want to create a slot in your `populate` method, you can use this method.\n\n        Args:\n            name (str): The name of the slot. If not passed, the default slot is inserted.\n            **kwargs: Additional keyword arguments to be passed to Slot initialization.\n\n        Returns:\n            Slot: The inserted slot object.\n        \"\"\"\n        if name in self.slots:\n            self.slots[name].parent = Tag.stack[-1]  # The children will be cleared during redraw, so re-establish\n        else:\n            self.slots[name] = Slot(ref=f\"slot={name}\", slot_name=name, page=self.page, parent=Tag.stack[-1], **kwargs)\n        slot = self.slots[name]\n        if self.origin:\n            slot.origin = self.origin\n            if slot.ref:\n                self.origin.refs[slot.ref] = slot\n        return slot\n\n    def slot(self, name=\"default\"):\n        \"\"\"\n        To be used in the `populate` method of code making use of this component, this method returns the slot object\n        with the given name. It should be used inside of a context manager.\n\n        Args:\n            name (str): The name of the slot to clear and return.\n\n        Returns:\n            Slot: The cleared slot object.\n        \"\"\"\n        #\n        # We put this here, so it clears the children only when the slot-filler is doing its filling.\n        # Otherwise, the previous children are kept. Lucky them.\n        self.slots[name].children = []\n        return self.slots[name]\n\n    def __enter__(self):\n        self.stack.append(self)\n        self.origin_stack[0].append(self)\n        self.component_stack.append(self)\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.stack.pop()\n        self.origin_stack[0].pop()\n        self.component_stack.pop()\n        return False\n\n    def __str__(self):\n        return f\"{self.component_name or self.__class__.__name__} ({self.ref} {id(self)})\"\n\n    def __repr__(self):\n        return f\"<{self}>\"\n
    "},{"location":"reference/component/#puepy.Component.initial","title":"initial()","text":"

    To be overridden in subclasses, the initial() method defines the initial state of the component.

    Returns:

    Type Description dict

    Initial component state

    Source code in puepy/core.py
    def initial(self):\n    \"\"\"\n    To be overridden in subclasses, the `initial()` method defines the initial state of the component.\n\n    Returns:\n        (dict): Initial component state\n    \"\"\"\n    return {}\n
    "},{"location":"reference/component/#puepy.Component.insert_slot","title":"insert_slot(name='default', **kwargs)","text":"

    In defining your own component, when you want to create a slot in your populate method, you can use this method.

    Parameters:

    Name Type Description Default name str

    The name of the slot. If not passed, the default slot is inserted.

    'default' **kwargs

    Additional keyword arguments to be passed to Slot initialization.

    {}

    Returns:

    Name Type Description Slot

    The inserted slot object.

    Source code in puepy/core.py
    def insert_slot(self, name=\"default\", **kwargs):\n    \"\"\"\n    In defining your own component, when you want to create a slot in your `populate` method, you can use this method.\n\n    Args:\n        name (str): The name of the slot. If not passed, the default slot is inserted.\n        **kwargs: Additional keyword arguments to be passed to Slot initialization.\n\n    Returns:\n        Slot: The inserted slot object.\n    \"\"\"\n    if name in self.slots:\n        self.slots[name].parent = Tag.stack[-1]  # The children will be cleared during redraw, so re-establish\n    else:\n        self.slots[name] = Slot(ref=f\"slot={name}\", slot_name=name, page=self.page, parent=Tag.stack[-1], **kwargs)\n    slot = self.slots[name]\n    if self.origin:\n        slot.origin = self.origin\n        if slot.ref:\n            self.origin.refs[slot.ref] = slot\n    return slot\n
    "},{"location":"reference/component/#puepy.Component.slot","title":"slot(name='default')","text":"

    To be used in the populate method of code making use of this component, this method returns the slot object with the given name. It should be used inside of a context manager.

    Parameters:

    Name Type Description Default name str

    The name of the slot to clear and return.

    'default'

    Returns:

    Name Type Description Slot

    The cleared slot object.

    Source code in puepy/core.py
    def slot(self, name=\"default\"):\n    \"\"\"\n    To be used in the `populate` method of code making use of this component, this method returns the slot object\n    with the given name. It should be used inside of a context manager.\n\n    Args:\n        name (str): The name of the slot to clear and return.\n\n    Returns:\n        Slot: The cleared slot object.\n    \"\"\"\n    #\n    # We put this here, so it clears the children only when the slot-filler is doing its filling.\n    # Otherwise, the previous children are kept. Lucky them.\n    self.slots[name].children = []\n    return self.slots[name]\n
    "},{"location":"reference/exceptions/","title":"peupy.exceptions","text":"

    Common exceptions in the PuePy framework.

    Classes:

    Name Description ElementNotInDom

    Raised when an element is not found in the DOM, but it is expected to be, such as when getting Tag.element

    PropsError

    Raised when unexpected props are passed to a component

    PageError

    Analogous to http errors, but for a single-page app where the error is client-side

    NotFound

    Page not found

    Forbidden

    Forbidden

    Unauthorized

    Unauthorized

    Redirect

    Redirect

    "},{"location":"reference/exceptions/#puepy.exceptions.ElementNotInDom","title":"ElementNotInDom","text":"

    Bases: Exception

    Raised when an element is not found in the DOM, but it is expected to be, such as when getting Tag.element

    Source code in puepy/exceptions.py
    class ElementNotInDom(Exception):\n    \"\"\"\n    Raised when an element is not found in the DOM, but it is expected to be, such as when getting Tag.element\n    \"\"\"\n\n    pass\n
    "},{"location":"reference/exceptions/#puepy.exceptions.Forbidden","title":"Forbidden","text":"

    Bases: PageError

    Raised manually, presumably when the user is not authorized to access a page.

    Source code in puepy/exceptions.py
    class Forbidden(PageError):\n    \"\"\"\n    Raised manually, presumably when the user is not authorized to access a page.\n    \"\"\"\n\n    def __str__(self):\n        return \"Forbidden\"\n
    "},{"location":"reference/exceptions/#puepy.exceptions.NotFound","title":"NotFound","text":"

    Bases: PageError

    Raised when the router could not find a page matching the user's URL.

    Source code in puepy/exceptions.py
    class NotFound(PageError):\n    \"\"\"\n    Raised when the router could not find a page matching the user's URL.\n    \"\"\"\n\n    def __str__(self):\n        return \"Page not found\"\n
    "},{"location":"reference/exceptions/#puepy.exceptions.PageError","title":"PageError","text":"

    Bases: Exception

    Analogous to http errors, but for a single-page app where the error is client-side

    Source code in puepy/exceptions.py
    class PageError(Exception):\n    \"\"\"\n    Analogous to http errors, but for a single-page app where the error is client-side\n    \"\"\"\n\n    pass\n
    "},{"location":"reference/exceptions/#puepy.exceptions.PropsError","title":"PropsError","text":"

    Bases: ValueError

    Raised when unexpected props are passed to a component

    Source code in puepy/exceptions.py
    class PropsError(ValueError):\n    \"\"\"\n    Raised when unexpected props are passed to a component\n    \"\"\"\n
    "},{"location":"reference/exceptions/#puepy.exceptions.Redirect","title":"Redirect","text":"

    Bases: PageError

    Raised manually when the user should be redirected to another page.

    Source code in puepy/exceptions.py
    class Redirect(PageError):\n    \"\"\"\n    Raised manually when the user should be redirected to another page.\n    \"\"\"\n\n    def __init__(self, path):\n        self.path = path\n\n    def __str__(self):\n        return f\"Redirect to {self.path}\"\n
    "},{"location":"reference/exceptions/#puepy.exceptions.Unauthorized","title":"Unauthorized","text":"

    Bases: PageError

    Raised manually, presumably when the user is not authenticated.

    Source code in puepy/exceptions.py
    class Unauthorized(PageError):\n    \"\"\"\n    Raised manually, presumably when the user is not authenticated.\n    \"\"\"\n\n    def __str__(self):\n        return \"Unauthorized\"\n
    "},{"location":"reference/prop/","title":"puepy.Prop","text":"

    Class representing a prop for a component.

    Attributes:

    Name Type Description name str

    The name of the property.

    description str

    The description of the property (optional).

    type type

    The data type of the property (default: str).

    default_value

    The default value of the property (optional).

    Source code in puepy/core.py
    class Prop:\n    \"\"\"\n    Class representing a prop for a component.\n\n    Attributes:\n        name (str): The name of the property.\n        description (str): The description of the property (optional).\n        type (type): The data type of the property (default: str).\n        default_value: The default value of the property (optional).\n    \"\"\"\n\n    def __init__(self, name, description=None, type=str, default_value=None):\n        self.name = name\n        self.description = description\n        self.type = type\n        self.default_value = default_value\n
    "},{"location":"reference/reactivity/","title":"puepy.reactivity","text":"

    Provides the base classes for PuePy's reactivity system independent of web concerns. These classes are not intended to be used directly, but could be useful for implementing a similar system in a different context.

    Classes:

    Name Description Listener

    A simple class that notifies a collection of callback functions when its notify method is called

    ReactiveDict

    A dictionary that notifies a listener when it is updated

    "},{"location":"reference/reactivity/#puepy.reactivity.Listener","title":"Listener","text":"

    A simple class that allows you to register callbacks and then notify them all at once.

    Attributes:

    Name Type Description callbacks list of callables

    A list of callback functions to be called when notify is called

    Source code in puepy/reactivity.py
    class Listener:\n    \"\"\"\n    A simple class that allows you to register callbacks and then notify them all at once.\n\n    Attributes:\n        callbacks (list of callables): A list of callback functions to be called when `notify` is called\n    \"\"\"\n\n    def __init__(self):\n        self.callbacks = []\n\n    def add_callback(self, callback):\n        \"\"\"\n        Adds a callback function to the listener.\n\n        Args:\n            callback (callable): The callback function to be added\n        \"\"\"\n        self.callbacks.append(callback)\n\n    def remove_callback(self, callback):\n        \"\"\"\n        Removes a callback function from the listener.\n\n        Args:\n            callback (callable): The callback to be removed\n        \"\"\"\n        self.callbacks.remove(callback)\n\n    def notify(self, *args, **kwargs):\n        \"\"\"\n        Notify method\n\n        Executes each callback function in the callbacks list by passing in the given arguments and keyword arguments.\n        If an exception occurs during the callback execution, it is logged using the logging library.\n\n        Args:\n            *args: Variable length argument list.\n            **kwargs: Arbitrary keyword arguments.\n        \"\"\"\n        for callback in self.callbacks:\n            try:\n                callback(*args, **kwargs)\n            except Exception as e:\n                logging.exception(\"Error in callback for {self}: {callback}:\".format(self=self, callback=callback))\n\n    def __str__(self):\n        if len(self.callbacks) == 1:\n            return f\"Listener: {self.callbacks[0]}\"\n        elif len(self.callbacks) > 1:\n            return f\"Listener with {len(self.callbacks)} callbacks\"\n        else:\n            return \"Listener with no callbacks\"\n\n    def __repr__(self):\n        return f\"<{self}>\"\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Listener.add_callback","title":"add_callback(callback)","text":"

    Adds a callback function to the listener.

    Parameters:

    Name Type Description Default callback callable

    The callback function to be added

    required Source code in puepy/reactivity.py
    def add_callback(self, callback):\n    \"\"\"\n    Adds a callback function to the listener.\n\n    Args:\n        callback (callable): The callback function to be added\n    \"\"\"\n    self.callbacks.append(callback)\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Listener.notify","title":"notify(*args, **kwargs)","text":"

    Notify method

    Executes each callback function in the callbacks list by passing in the given arguments and keyword arguments. If an exception occurs during the callback execution, it is logged using the logging library.

    Parameters:

    Name Type Description Default *args

    Variable length argument list.

    () **kwargs

    Arbitrary keyword arguments.

    {} Source code in puepy/reactivity.py
    def notify(self, *args, **kwargs):\n    \"\"\"\n    Notify method\n\n    Executes each callback function in the callbacks list by passing in the given arguments and keyword arguments.\n    If an exception occurs during the callback execution, it is logged using the logging library.\n\n    Args:\n        *args: Variable length argument list.\n        **kwargs: Arbitrary keyword arguments.\n    \"\"\"\n    for callback in self.callbacks:\n        try:\n            callback(*args, **kwargs)\n        except Exception as e:\n            logging.exception(\"Error in callback for {self}: {callback}:\".format(self=self, callback=callback))\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Listener.remove_callback","title":"remove_callback(callback)","text":"

    Removes a callback function from the listener.

    Parameters:

    Name Type Description Default callback callable

    The callback to be removed

    required Source code in puepy/reactivity.py
    def remove_callback(self, callback):\n    \"\"\"\n    Removes a callback function from the listener.\n\n    Args:\n        callback (callable): The callback to be removed\n    \"\"\"\n    self.callbacks.remove(callback)\n
    "},{"location":"reference/reactivity/#puepy.reactivity.ReactiveDict","title":"ReactiveDict","text":"

    Bases: dict

    A dictionary that notifies a listener when it is updated.

    Attributes:

    Name Type Description listener Listener

    A listener object that is notified when the dictionary is updated

    key_listeners dict

    A dictionary of listeners that are notified when a specific key is updated

    Source code in puepy/reactivity.py
    class ReactiveDict(dict):\n    \"\"\"\n    A dictionary that notifies a listener when it is updated.\n\n    Attributes:\n        listener (Listener): A listener object that is notified when the dictionary is updated\n        key_listeners (dict): A dictionary of listeners that are notified when a specific key is updated\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args)\n        self.listener = Listener()\n        self.key_listeners = {}\n        self._in_mutation = False\n        self._notifications_pending = set()\n        self._keys_mutate = None\n\n    def add_key_listener(self, key, callback):\n        \"\"\"\n        Adds a key listener to the object.\n\n        Args:\n            key (str): The key for which the listener will be added.\n            callback (callable): The callback function to be executed when the key event is triggered.\n        \"\"\"\n        if key not in self.key_listeners:\n            self.key_listeners[key] = Listener()\n        self.key_listeners[key].add_callback(callback)\n\n    def notify(self, *keys):\n        \"\"\"\n        Notifies the listener and key listeners that the object has been updated.\n\n        Args:\n            *keys: A variable number of keys to be modified for key-specific listeners.\n        \"\"\"\n        if keys:\n            self._notifications_pending.update(keys)\n        else:\n            self._notifications_pending.update(self.keys())\n\n        if not self._in_mutation:\n            self._flush_pending()\n\n    def mutate(self, *keys):\n        \"\"\"\n        To be used as a context manager, this method is for either deferring all notifications until a change has been completed and/or notifying listeners when \"deep\" changes are made that would have gone undetected by `__setitem__`.\n\n        Examples:\n            ``` py\n            with reactive_dict.mutate(\"my_list\", \"my_dict\"):\n                reactive_dict[\"my_list\"].append(\"spam\")\n                reactive_dict[\"my_dict\"][\"spam\"] = \"eggs\"\n            ```\n\n        Args:\n            *keys: A variable number of keys to update the notifications pending attribute with. If no keys are provided, all keys in the object will be updated.\n\n        Returns:\n            The reactive dict itself, which stylistically could be nice to use in a `with` statement.\n        \"\"\"\n        if keys:\n            self._notifications_pending.update(keys)\n        else:\n            self._notifications_pending.update(self.keys())\n        self._keys_mutate = keys\n        return self\n\n    def update(self, other):\n        with self.mutate(*other.keys()):\n            super().update(other)\n\n    def _flush_pending(self):\n        while self._notifications_pending:\n            key = self._notifications_pending.pop()\n            value = self.get(key, None)\n            self.listener.notify(key, value)\n            if key in self.key_listeners:\n                self.key_listeners[key].notify(key, value)\n\n    def __setitem__(self, key, value):\n        if (key in self and value != self[key]) or key not in self:\n            super().__setitem__(key, value)\n            self.notify(key)\n\n    def __delitem__(self, key):\n        super().__delitem__(key)\n        self.notify(key)\n\n    def __enter__(self):\n        self._in_mutation = True\n\n        if len(self._keys_mutate) == 0:\n            return self.get(self._keys_mutate)\n        elif len(self._keys_mutate) > 1:\n            return [self.get(k) for k in self._keys_mutate]\n\n    def __exit__(self, type, value, traceback):\n        self._in_mutation = False\n        self._flush_pending()\n
    "},{"location":"reference/reactivity/#puepy.reactivity.ReactiveDict.add_key_listener","title":"add_key_listener(key, callback)","text":"

    Adds a key listener to the object.

    Parameters:

    Name Type Description Default key str

    The key for which the listener will be added.

    required callback callable

    The callback function to be executed when the key event is triggered.

    required Source code in puepy/reactivity.py
    def add_key_listener(self, key, callback):\n    \"\"\"\n    Adds a key listener to the object.\n\n    Args:\n        key (str): The key for which the listener will be added.\n        callback (callable): The callback function to be executed when the key event is triggered.\n    \"\"\"\n    if key not in self.key_listeners:\n        self.key_listeners[key] = Listener()\n    self.key_listeners[key].add_callback(callback)\n
    "},{"location":"reference/reactivity/#puepy.reactivity.ReactiveDict.mutate","title":"mutate(*keys)","text":"

    To be used as a context manager, this method is for either deferring all notifications until a change has been completed and/or notifying listeners when \"deep\" changes are made that would have gone undetected by __setitem__.

    Examples:

    with reactive_dict.mutate(\"my_list\", \"my_dict\"):\n    reactive_dict[\"my_list\"].append(\"spam\")\n    reactive_dict[\"my_dict\"][\"spam\"] = \"eggs\"\n

    Parameters:

    Name Type Description Default *keys

    A variable number of keys to update the notifications pending attribute with. If no keys are provided, all keys in the object will be updated.

    ()

    Returns:

    Type Description

    The reactive dict itself, which stylistically could be nice to use in a with statement.

    Source code in puepy/reactivity.py
    def mutate(self, *keys):\n    \"\"\"\n    To be used as a context manager, this method is for either deferring all notifications until a change has been completed and/or notifying listeners when \"deep\" changes are made that would have gone undetected by `__setitem__`.\n\n    Examples:\n        ``` py\n        with reactive_dict.mutate(\"my_list\", \"my_dict\"):\n            reactive_dict[\"my_list\"].append(\"spam\")\n            reactive_dict[\"my_dict\"][\"spam\"] = \"eggs\"\n        ```\n\n    Args:\n        *keys: A variable number of keys to update the notifications pending attribute with. If no keys are provided, all keys in the object will be updated.\n\n    Returns:\n        The reactive dict itself, which stylistically could be nice to use in a `with` statement.\n    \"\"\"\n    if keys:\n        self._notifications_pending.update(keys)\n    else:\n        self._notifications_pending.update(self.keys())\n    self._keys_mutate = keys\n    return self\n
    "},{"location":"reference/reactivity/#puepy.reactivity.ReactiveDict.notify","title":"notify(*keys)","text":"

    Notifies the listener and key listeners that the object has been updated.

    Parameters:

    Name Type Description Default *keys

    A variable number of keys to be modified for key-specific listeners.

    () Source code in puepy/reactivity.py
    def notify(self, *keys):\n    \"\"\"\n    Notifies the listener and key listeners that the object has been updated.\n\n    Args:\n        *keys: A variable number of keys to be modified for key-specific listeners.\n    \"\"\"\n    if keys:\n        self._notifications_pending.update(keys)\n    else:\n        self._notifications_pending.update(self.keys())\n\n    if not self._in_mutation:\n        self._flush_pending()\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Stateful","title":"Stateful","text":"

    A class that provides a reactive state management system for components. A

    Source code in puepy/reactivity.py
    class Stateful:\n    \"\"\"\n    A class that provides a reactive state management system for components. A\n    \"\"\"\n\n    def add_context(self, name: str, value: ReactiveDict):\n        \"\"\"\n        Adds contxt from a reactive dict to be reacted on by the component.\n        \"\"\"\n        value.listener.add_callback(partial(self._on_state_change, name))\n\n    def initial(self):\n        \"\"\"\n        To be overridden in subclasses, the `initial()` method defines the initial state of the stateful object.\n\n        Returns:\n            (dict): Initial component state\n        \"\"\"\n        return {}\n\n    def on_state_change(self, context, key, value):\n        \"\"\"\n        To be overridden in subclasses, this method is called whenever the state of the component changes.\n\n        Args:\n            context: What context the state change occured in\n            key: The key modified\n            value: The new value\n        \"\"\"\n        pass\n\n    def _on_state_change(self, context, key, value):\n        self.on_state_change(context, key, value)\n\n        if hasattr(self, f\"on_{key}_change\"):\n            getattr(self, f\"on_{key}_change\")(value)\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Stateful.add_context","title":"add_context(name, value)","text":"

    Adds contxt from a reactive dict to be reacted on by the component.

    Source code in puepy/reactivity.py
    def add_context(self, name: str, value: ReactiveDict):\n    \"\"\"\n    Adds contxt from a reactive dict to be reacted on by the component.\n    \"\"\"\n    value.listener.add_callback(partial(self._on_state_change, name))\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Stateful.initial","title":"initial()","text":"

    To be overridden in subclasses, the initial() method defines the initial state of the stateful object.

    Returns:

    Type Description dict

    Initial component state

    Source code in puepy/reactivity.py
    def initial(self):\n    \"\"\"\n    To be overridden in subclasses, the `initial()` method defines the initial state of the stateful object.\n\n    Returns:\n        (dict): Initial component state\n    \"\"\"\n    return {}\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Stateful.on_state_change","title":"on_state_change(context, key, value)","text":"

    To be overridden in subclasses, this method is called whenever the state of the component changes.

    Parameters:

    Name Type Description Default context

    What context the state change occured in

    required key

    The key modified

    required value

    The new value

    required Source code in puepy/reactivity.py
    def on_state_change(self, context, key, value):\n    \"\"\"\n    To be overridden in subclasses, this method is called whenever the state of the component changes.\n\n    Args:\n        context: What context the state change occured in\n        key: The key modified\n        value: The new value\n    \"\"\"\n    pass\n
    "},{"location":"reference/router/","title":"puepy.router","text":"

    The puepy.router module contains code relevant to optional client-side routing in PuePy.

    See Also

    • Tutorial: Routing
    • Guide: Advanced Routing

    PuePy's router functionality can be optionally installed by calling the install_router method of the Application class.

    Example
    from puepy import Application, Router\n\napp = Application()\napp.install_router(Router, link_mode=Router.LINK_MODE_HASH)\n

    Once installed, the Router instance is available on app.Router and can be used throughout the application to manage client-side routing. Routes are defined by either using the @app.page decorator or by calling methods manually on the Router instance.

    Classes:

    Name Description puepy.router.Route

    Represents a route in the router.

    puepy.router.Router

    Represents a router for managing client-side routing in a web application.

    "},{"location":"reference/router/#puepy.router.Route","title":"Route","text":"

    Represents a route in the router. A route is defined by a path match pattern, a page class, and a name.

    Note

    This is usually not instanciated directly. Instead, use the Router.add_route method to create a new route or use the @app.page decorator to define a route at the time you define your Pages.

    Source code in puepy/router.py
    class Route:\n    \"\"\"\n    Represents a route in the router. A route is defined by a path match pattern, a page class, and a name.\n\n    Note:\n        This is usually not instanciated directly. Instead, use the `Router.add_route` method to create a new route or\n        use the @app.page decorator to define a route at the time you define your Pages.\n    \"\"\"\n\n    def __init__(self, path_match: str, page: Page, name: str, base_path: str, router=None):\n        \"\"\"\n        Args:\n            path_match (str): The path match pattern used for routing.\n            page (Page): An instance of the Page class representing the page.\n            name (str): The name of the page.\n            base_path (str): The base path used for routing.\n            router (Router, optional): An optional parameter representing the router used for routing.\n        \"\"\"\n        self.path_match = path_match\n        self.page = page\n        self.name = name\n        self.base_path = base_path\n        self.router = router\n\n    def match(self, path):\n        \"\"\"\n        Evaluates a path against the route's pattern to determine if there is a match.\n\n        Args:\n            path: The path to be matched against the pattern.\n\n        Returns:\n            Match found (tuple): A tuple containing a True boolean value and a dictionary. The dictionary contains the\n            matched variables extracted from the path.\n\n            Match not found (tuple): If no match is found, returns `(False, None)`.\n        \"\"\"\n        if self.base_path and path.startswith(self.base_path):\n            path = path[len(self.base_path) :]\n\n        # Simple pattern matching without regex\n        parts = path.strip(\"/\").split(\"/\")\n        pattern_parts = self.path_match.strip(\"/\").split(\"/\")\n        if len(parts) != len(pattern_parts):\n            return False, None\n\n        kwargs = {}\n        for part, pattern_part in zip(parts, pattern_parts):\n            if pattern_part.startswith(\"<\") and pattern_part.endswith(\">\"):\n                group_name = pattern_part[1:-1]\n                kwargs[group_name] = part\n            elif part != pattern_part:\n                return False, None\n\n        return True, kwargs\n\n    def reverse(self, **kwargs):\n        \"\"\"\n        Reverse method is used to generate a URL path using the given parameters. It replaces the placeholders in the\n        path template with the corresponding values.\n\n        Args:\n            **kwargs: A variable number of keyword arguments representing the values to be inserted into the path\n            template.\n\n        Returns:\n            (str): The generated URL path.\n\n        Example:\n            Let's say we have a path template `/users/<username>/posts/<post_id>`. We can use the reverse method to\n            generate the URL path by providing the values for \"username\" and \"post_id\" as keyword arguments:\n            `route.reverse(username=\"john\", post_id=123)` => `\"/users/john/posts/123\"`\n        \"\"\"\n        kwargs = kwargs.copy()\n        result = self.path_match\n        for key in list(kwargs.keys()):\n            if f\"<{key}>\" in result:\n                value = kwargs.pop(key)\n                result = result.replace(f\"<{key}>\", str(value))\n\n        if self.router and self.router.link_mode == Router.LINK_MODE_HASH:\n            result = \"#\" + result\n\n        if self.base_path:\n            path = f\"{self.base_path}{result}\"\n        else:\n            path = result\n\n        if kwargs:\n            path += \"?\" + \"&\".join(f\"{url_quote(k)}={url_quote(v)}\" for k, v in kwargs.items())\n        return path\n\n    def __str__(self):\n        return self.name\n\n    def __repr__(self):\n        return f\"<Route: {self.name}>\"\n
    "},{"location":"reference/router/#puepy.router.Route.__init__","title":"__init__(path_match, page, name, base_path, router=None)","text":"

    Parameters:

    Name Type Description Default path_match str

    The path match pattern used for routing.

    required page Page

    An instance of the Page class representing the page.

    required name str

    The name of the page.

    required base_path str

    The base path used for routing.

    required router Router

    An optional parameter representing the router used for routing.

    None Source code in puepy/router.py
    def __init__(self, path_match: str, page: Page, name: str, base_path: str, router=None):\n    \"\"\"\n    Args:\n        path_match (str): The path match pattern used for routing.\n        page (Page): An instance of the Page class representing the page.\n        name (str): The name of the page.\n        base_path (str): The base path used for routing.\n        router (Router, optional): An optional parameter representing the router used for routing.\n    \"\"\"\n    self.path_match = path_match\n    self.page = page\n    self.name = name\n    self.base_path = base_path\n    self.router = router\n
    "},{"location":"reference/router/#puepy.router.Route.match","title":"match(path)","text":"

    Evaluates a path against the route's pattern to determine if there is a match.

    Parameters:

    Name Type Description Default path

    The path to be matched against the pattern.

    required

    Returns:

    Type Description

    Match found (tuple): A tuple containing a True boolean value and a dictionary. The dictionary contains the

    matched variables extracted from the path.

    Match not found (tuple): If no match is found, returns (False, None).

    Source code in puepy/router.py
    def match(self, path):\n    \"\"\"\n    Evaluates a path against the route's pattern to determine if there is a match.\n\n    Args:\n        path: The path to be matched against the pattern.\n\n    Returns:\n        Match found (tuple): A tuple containing a True boolean value and a dictionary. The dictionary contains the\n        matched variables extracted from the path.\n\n        Match not found (tuple): If no match is found, returns `(False, None)`.\n    \"\"\"\n    if self.base_path and path.startswith(self.base_path):\n        path = path[len(self.base_path) :]\n\n    # Simple pattern matching without regex\n    parts = path.strip(\"/\").split(\"/\")\n    pattern_parts = self.path_match.strip(\"/\").split(\"/\")\n    if len(parts) != len(pattern_parts):\n        return False, None\n\n    kwargs = {}\n    for part, pattern_part in zip(parts, pattern_parts):\n        if pattern_part.startswith(\"<\") and pattern_part.endswith(\">\"):\n            group_name = pattern_part[1:-1]\n            kwargs[group_name] = part\n        elif part != pattern_part:\n            return False, None\n\n    return True, kwargs\n
    "},{"location":"reference/router/#puepy.router.Route.reverse","title":"reverse(**kwargs)","text":"

    Reverse method is used to generate a URL path using the given parameters. It replaces the placeholders in the path template with the corresponding values.

    Parameters:

    Name Type Description Default **kwargs

    A variable number of keyword arguments representing the values to be inserted into the path

    {}

    Returns:

    Type Description str

    The generated URL path.

    Example

    Let's say we have a path template /users/<username>/posts/<post_id>. We can use the reverse method to generate the URL path by providing the values for \"username\" and \"post_id\" as keyword arguments: route.reverse(username=\"john\", post_id=123) => \"/users/john/posts/123\"

    Source code in puepy/router.py
    def reverse(self, **kwargs):\n    \"\"\"\n    Reverse method is used to generate a URL path using the given parameters. It replaces the placeholders in the\n    path template with the corresponding values.\n\n    Args:\n        **kwargs: A variable number of keyword arguments representing the values to be inserted into the path\n        template.\n\n    Returns:\n        (str): The generated URL path.\n\n    Example:\n        Let's say we have a path template `/users/<username>/posts/<post_id>`. We can use the reverse method to\n        generate the URL path by providing the values for \"username\" and \"post_id\" as keyword arguments:\n        `route.reverse(username=\"john\", post_id=123)` => `\"/users/john/posts/123\"`\n    \"\"\"\n    kwargs = kwargs.copy()\n    result = self.path_match\n    for key in list(kwargs.keys()):\n        if f\"<{key}>\" in result:\n            value = kwargs.pop(key)\n            result = result.replace(f\"<{key}>\", str(value))\n\n    if self.router and self.router.link_mode == Router.LINK_MODE_HASH:\n        result = \"#\" + result\n\n    if self.base_path:\n        path = f\"{self.base_path}{result}\"\n    else:\n        path = result\n\n    if kwargs:\n        path += \"?\" + \"&\".join(f\"{url_quote(k)}={url_quote(v)}\" for k, v in kwargs.items())\n    return path\n
    "},{"location":"reference/router/#puepy.router.Router","title":"Router","text":"

    Class representing a router for managing client-side routing in a web application.

    Parameters:

    Name Type Description Default application object

    The web application object. Defaults to None.

    None base_path str

    The base path URL. Defaults to None.

    None link_mode str

    The link mode for navigating. Defaults to \"hash\".

    LINK_MODE_HASH

    Attributes:

    Name Type Description LINK_MODE_DIRECT str

    Direct link mode.

    LINK_MODE_HTML5 str

    HTML5 link mode.

    LINK_MODE_HASH str

    Hash link mode.

    routes list

    List of Route instances.

    routes_by_name dict

    Dictionary mapping route names to Route instances.

    routes_by_page dict

    Dictionary mapping page classes to Route instances.

    application object

    The web application object.

    base_path str

    The base path URL.

    link_mode str

    The link mode for navigating.

    Source code in puepy/router.py
    class Router:\n    \"\"\"Class representing a router for managing client-side routing in a web application.\n\n\n\n    Args:\n        application (object, optional): The web application object. Defaults to None.\n        base_path (str, optional): The base path URL. Defaults to None.\n        link_mode (str, optional): The link mode for navigating. Defaults to \"hash\".\n\n    Attributes:\n        LINK_MODE_DIRECT (str): Direct link mode.\n        LINK_MODE_HTML5 (str): HTML5 link mode.\n        LINK_MODE_HASH (str): Hash link mode.\n        routes (list): List of Route instances.\n        routes_by_name (dict): Dictionary mapping route names to Route instances.\n        routes_by_page (dict): Dictionary mapping page classes to Route instances.\n        application (object): The web application object.\n        base_path (str): The base path URL.\n        link_mode (str): The link mode for navigating.\n    \"\"\"\n\n    LINK_MODE_DIRECT = \"direct\"\n    LINK_MODE_HTML5 = \"html5\"\n    LINK_MODE_HASH = \"hash\"\n\n    def __init__(self, application=None, base_path=None, link_mode=LINK_MODE_HASH):\n        \"\"\"\n        Initializes an instance of the class.\n\n        Parameters:\n            application (Application): The application used for routing.\n            base_path (str): The base path for the routes.\n            link_mode (str): The mode for generating links.\n        \"\"\"\n        self.routes = []\n        self.routes_by_name = {}\n        self.routes_by_page = {}\n        self.application = application\n        self.base_path = base_path\n        self.link_mode = link_mode\n\n    def add_route_instance(self, route: Route):\n        \"\"\"\n        Add a route instance to the current router.\n\n        Parameters:\n            route (Route): The route instance to be added.\n\n        Raises:\n            ValueError: If the route instance or route name already exists in the router.\n        \"\"\"\n        if route in self.routes:\n            raise ValueError(f\"Route already added: {route}\")\n        if route.name in self.routes_by_name:\n            raise ValueError(f\"Route name already exists for another route: {route.name}\")\n        self.routes.append(route)\n        self.routes_by_name[route.name] = route\n        self.routes_by_page[route.page] = route\n        route.router = self\n\n    def add_route(self, path_match, page_class, name=None):\n        \"\"\"\n        Adds a route to the router. This method creates a new Route instance.\n\n        Args:\n            path_match (str): The URL path pattern to match for the route.\n            page_class (Page class): The class or function to be associated with the route.\n            name (str, optional): The name of the route. If not provided, the name will be derived from the page class name.\n        \"\"\"\n        # Convert path to a simple pattern without regex\n        if not name:\n            name = mixed_to_underscores(page_class.__name__)\n        self.add_route_instance(Route(path_match=path_match, page=page_class, name=name, base_path=self.base_path))\n\n    def reverse(self, destination, **kwargs):\n        \"\"\"\n        Reverses a\n\n        Args:\n            destination: The destination to reverse. It can be the name of a route, the mapped page of a route, or the default page of the application.\n            **kwargs: Additional keyword arguments to be passed to the reverse method of the destination route.\n\n        Returns:\n            (str): The reversed URL for the given destination.\n\n        Raises:\n            KeyError: If the destination is not found in the routes.\n        \"\"\"\n        route: Route\n        if isinstance(destination, Route):\n            return destination.reverse(**kwargs)\n        elif destination in self.routes_by_name:\n            route = self.routes_by_name[destination]\n        elif destination in self.routes_by_page:\n            route = self.routes_by_page[destination]\n        elif self.application and destination is self.application.default_page:\n            if self.link_mode == Router.LINK_MODE_HASH:\n                path = \"#/\"\n            else:\n                path = \"/\"\n            return self.base_path or \"\" + path\n        else:\n            raise KeyError(f\"{destination} not found in routes\")\n        return route.reverse(**kwargs)\n\n    def match(self, path):\n        \"\"\"\n        Args:\n            path (str): The path to be matched.\n\n        Returns:\n            (tuple): A tuple containing the matching route and the matched route arguments (if any). If no route is\n                found, returns (None, None).\n        \"\"\"\n        path = path.split(\"#\")[0]\n        if \"?\" not in path:\n            path += \"?\"\n        path, query_string = path.split(\"?\", 1)\n        arguments = parse_query_string(query_string)\n\n        for route in self.routes:\n            matches, path_arguments = route.match(path)\n            if path_arguments:\n                arguments.update(path_arguments)\n            if matches:\n                return route, arguments\n        return None, None\n\n    def navigate_to_path(self, path, **kwargs):\n        \"\"\"\n        Navigates to the specified path.\n\n        Args:\n            path (str or Page): The path to navigate to. If path is a subclass of Page, it will be reversed using the reverse method\n            provided by the self object. If path is a string and **kwargs is not empty, it will append the query string\n            to the path.\n\n            **kwargs: Additional key-value pairs to be included in the query string. Each key-value pair will be\n            URL-encoded.\n\n        Raises:\n            Exception: If the link mode is invalid.\n        \"\"\"\n        if isinstance(path, type) and issubclass(path, Page):\n            path = self.reverse(path, **kwargs)\n        elif kwargs:\n            path += \"?\" + \"&\".join(f\"{url_quote(k)}={url_quote(v)}\" for k, v in kwargs.items())\n\n        if self.link_mode == self.LINK_MODE_DIRECT:\n            window.location = path\n        elif self.link_mode == self.LINK_MODE_HTML5:\n            history.pushState(jsobj(), \"\", path)\n            self.application.mount(self.application._selector_or_element, path)\n        elif self.link_mode == self.LINK_MODE_HASH:\n            path = path[1:] if path.startswith(\"#\") else path\n            if not is_server_side:\n                history.pushState(jsobj(), \"\", \"#\" + path)\n            self.application.mount(self.application._selector_or_element, path)\n        else:\n            raise Exception(f\"Invalid link mode: {self.link_mode}\")\n
    "},{"location":"reference/router/#puepy.router.Router.__init__","title":"__init__(application=None, base_path=None, link_mode=LINK_MODE_HASH)","text":"

    Initializes an instance of the class.

    Parameters:

    Name Type Description Default application Application

    The application used for routing.

    None base_path str

    The base path for the routes.

    None link_mode str

    The mode for generating links.

    LINK_MODE_HASH Source code in puepy/router.py
    def __init__(self, application=None, base_path=None, link_mode=LINK_MODE_HASH):\n    \"\"\"\n    Initializes an instance of the class.\n\n    Parameters:\n        application (Application): The application used for routing.\n        base_path (str): The base path for the routes.\n        link_mode (str): The mode for generating links.\n    \"\"\"\n    self.routes = []\n    self.routes_by_name = {}\n    self.routes_by_page = {}\n    self.application = application\n    self.base_path = base_path\n    self.link_mode = link_mode\n
    "},{"location":"reference/router/#puepy.router.Router.add_route","title":"add_route(path_match, page_class, name=None)","text":"

    Adds a route to the router. This method creates a new Route instance.

    Parameters:

    Name Type Description Default path_match str

    The URL path pattern to match for the route.

    required page_class Page class

    The class or function to be associated with the route.

    required name str

    The name of the route. If not provided, the name will be derived from the page class name.

    None Source code in puepy/router.py
    def add_route(self, path_match, page_class, name=None):\n    \"\"\"\n    Adds a route to the router. This method creates a new Route instance.\n\n    Args:\n        path_match (str): The URL path pattern to match for the route.\n        page_class (Page class): The class or function to be associated with the route.\n        name (str, optional): The name of the route. If not provided, the name will be derived from the page class name.\n    \"\"\"\n    # Convert path to a simple pattern without regex\n    if not name:\n        name = mixed_to_underscores(page_class.__name__)\n    self.add_route_instance(Route(path_match=path_match, page=page_class, name=name, base_path=self.base_path))\n
    "},{"location":"reference/router/#puepy.router.Router.add_route_instance","title":"add_route_instance(route)","text":"

    Add a route instance to the current router.

    Parameters:

    Name Type Description Default route Route

    The route instance to be added.

    required

    Raises:

    Type Description ValueError

    If the route instance or route name already exists in the router.

    Source code in puepy/router.py
    def add_route_instance(self, route: Route):\n    \"\"\"\n    Add a route instance to the current router.\n\n    Parameters:\n        route (Route): The route instance to be added.\n\n    Raises:\n        ValueError: If the route instance or route name already exists in the router.\n    \"\"\"\n    if route in self.routes:\n        raise ValueError(f\"Route already added: {route}\")\n    if route.name in self.routes_by_name:\n        raise ValueError(f\"Route name already exists for another route: {route.name}\")\n    self.routes.append(route)\n    self.routes_by_name[route.name] = route\n    self.routes_by_page[route.page] = route\n    route.router = self\n
    "},{"location":"reference/router/#puepy.router.Router.match","title":"match(path)","text":"

    Parameters:

    Name Type Description Default path str

    The path to be matched.

    required

    Returns:

    Type Description tuple

    A tuple containing the matching route and the matched route arguments (if any). If no route is found, returns (None, None).

    Source code in puepy/router.py
    def match(self, path):\n    \"\"\"\n    Args:\n        path (str): The path to be matched.\n\n    Returns:\n        (tuple): A tuple containing the matching route and the matched route arguments (if any). If no route is\n            found, returns (None, None).\n    \"\"\"\n    path = path.split(\"#\")[0]\n    if \"?\" not in path:\n        path += \"?\"\n    path, query_string = path.split(\"?\", 1)\n    arguments = parse_query_string(query_string)\n\n    for route in self.routes:\n        matches, path_arguments = route.match(path)\n        if path_arguments:\n            arguments.update(path_arguments)\n        if matches:\n            return route, arguments\n    return None, None\n
    "},{"location":"reference/router/#puepy.router.Router.navigate_to_path","title":"navigate_to_path(path, **kwargs)","text":"

    Navigates to the specified path.

    Parameters:

    Name Type Description Default path str or Page

    The path to navigate to. If path is a subclass of Page, it will be reversed using the reverse method

    required **kwargs

    Additional key-value pairs to be included in the query string. Each key-value pair will be

    {}

    Raises:

    Type Description Exception

    If the link mode is invalid.

    Source code in puepy/router.py
    def navigate_to_path(self, path, **kwargs):\n    \"\"\"\n    Navigates to the specified path.\n\n    Args:\n        path (str or Page): The path to navigate to. If path is a subclass of Page, it will be reversed using the reverse method\n        provided by the self object. If path is a string and **kwargs is not empty, it will append the query string\n        to the path.\n\n        **kwargs: Additional key-value pairs to be included in the query string. Each key-value pair will be\n        URL-encoded.\n\n    Raises:\n        Exception: If the link mode is invalid.\n    \"\"\"\n    if isinstance(path, type) and issubclass(path, Page):\n        path = self.reverse(path, **kwargs)\n    elif kwargs:\n        path += \"?\" + \"&\".join(f\"{url_quote(k)}={url_quote(v)}\" for k, v in kwargs.items())\n\n    if self.link_mode == self.LINK_MODE_DIRECT:\n        window.location = path\n    elif self.link_mode == self.LINK_MODE_HTML5:\n        history.pushState(jsobj(), \"\", path)\n        self.application.mount(self.application._selector_or_element, path)\n    elif self.link_mode == self.LINK_MODE_HASH:\n        path = path[1:] if path.startswith(\"#\") else path\n        if not is_server_side:\n            history.pushState(jsobj(), \"\", \"#\" + path)\n        self.application.mount(self.application._selector_or_element, path)\n    else:\n        raise Exception(f\"Invalid link mode: {self.link_mode}\")\n
    "},{"location":"reference/router/#puepy.router.Router.reverse","title":"reverse(destination, **kwargs)","text":"

    Reverses a

    Parameters:

    Name Type Description Default destination

    The destination to reverse. It can be the name of a route, the mapped page of a route, or the default page of the application.

    required **kwargs

    Additional keyword arguments to be passed to the reverse method of the destination route.

    {}

    Returns:

    Type Description str

    The reversed URL for the given destination.

    Raises:

    Type Description KeyError

    If the destination is not found in the routes.

    Source code in puepy/router.py
    def reverse(self, destination, **kwargs):\n    \"\"\"\n    Reverses a\n\n    Args:\n        destination: The destination to reverse. It can be the name of a route, the mapped page of a route, or the default page of the application.\n        **kwargs: Additional keyword arguments to be passed to the reverse method of the destination route.\n\n    Returns:\n        (str): The reversed URL for the given destination.\n\n    Raises:\n        KeyError: If the destination is not found in the routes.\n    \"\"\"\n    route: Route\n    if isinstance(destination, Route):\n        return destination.reverse(**kwargs)\n    elif destination in self.routes_by_name:\n        route = self.routes_by_name[destination]\n    elif destination in self.routes_by_page:\n        route = self.routes_by_page[destination]\n    elif self.application and destination is self.application.default_page:\n        if self.link_mode == Router.LINK_MODE_HASH:\n            path = \"#/\"\n        else:\n            path = \"/\"\n        return self.base_path or \"\" + path\n    else:\n        raise KeyError(f\"{destination} not found in routes\")\n    return route.reverse(**kwargs)\n
    "},{"location":"reference/router/#puepy.router._micropython_parse_query_string","title":"_micropython_parse_query_string(query_string)","text":"

    In MicroPython, urllib isn't available and we can't use the JavaScript library: https://github.com/pyscript/pyscript/issues/2100

    Source code in puepy/router.py
    def _micropython_parse_query_string(query_string):\n    \"\"\"\n    In MicroPython, urllib isn't available and we can't use the JavaScript library:\n    https://github.com/pyscript/pyscript/issues/2100\n    \"\"\"\n    if query_string and query_string[0] == \"?\":\n        query_string = query_string[1:]\n\n    def url_decode(s):\n        # Decode URL-encoded characters without using regex, which is also pretty broken in MicroPython...\n        i = 0\n        length = len(s)\n        decoded = []\n\n        while i < length:\n            if s[i] == \"%\":\n                if i + 2 < length:\n                    hex_value = s[i + 1 : i + 3]\n                    decoded.append(chr(int(hex_value, 16)))\n                    i += 3\n                else:\n                    decoded.append(\"%\")\n                    i += 1\n            elif s[i] == \"+\":\n                decoded.append(\" \")\n                i += 1\n            else:\n                decoded.append(s[i])\n                i += 1\n\n        return \"\".join(decoded)\n\n    params = {}\n    for part in query_string.split(\"&\"):\n        if \"=\" in part:\n            key, value = part.split(\"=\", 1)\n            key = url_decode(key)\n            value = url_decode(value)\n            if key in params:\n                params[key].append(value)\n            else:\n                params[key] = [value]\n        else:\n            key = url_decode(part)\n            if key in params:\n                params[key].append(\"\")\n            else:\n                params[key] = \"\"\n    return params\n
    "},{"location":"reference/storage/","title":"puepy.storage","text":""},{"location":"reference/storage/#puepystorage","title":"puepy.storage","text":"

    Browser Storage Module

    This module provides a BrowserStorage class that interfaces with browser storage objects such as localStorage and sessionStorage. It mimics dictionary-like behavior for interacting with storage items.

    Classes:

    Name Description BrowserStorage

    A class that provides dictionary-like access to browser storage objects.

    "},{"location":"reference/storage/#puepy.storage.BrowserStorage","title":"BrowserStorage","text":"

    Provides dictionary-like interface to browser storage objects.

    Attributes:

    Name Type Description target

    The browser storage object (e.g., localStorage, sessionStorage).

    description str

    Description of the storage instance.

    Source code in puepy/storage.py
    class BrowserStorage:\n    \"\"\"\n    Provides dictionary-like interface to browser storage objects.\n\n    Attributes:\n        target: The browser storage object (e.g., localStorage, sessionStorage).\n        description (str): Description of the storage instance.\n\n    \"\"\"\n\n    class NoDefault:\n        \"\"\"Placeholder class for default values when no default is provided.\"\"\"\n\n        pass\n\n    def __init__(self, target, description):\n        \"\"\"\n        Initializes the BrowserStorage instance.\n\n        Args:\n            target: The browser storage object.\n            description (str): Description of the storage instance.\n        \"\"\"\n        self.target = target\n        self.description = description\n\n    def __getitem__(self, key):\n        \"\"\"\n        Retrieves the value for a given key from the storage.\n\n        Args:\n            key (str): The key for the item to retrieve.\n\n        Returns:\n            The value associated with the key.\n\n        Raises:\n            KeyError: If the key does not exist in the storage.\n        \"\"\"\n        value = self.target.getItem(key)\n        if value is None:\n            raise KeyError(key)\n        return value\n\n    def __setitem__(self, key, value):\n        \"\"\"\n        Sets the value for a given key in the storage.\n\n        Args:\n            key (str): The key for the item to set.\n            value: The value to associate with the key.\n        \"\"\"\n        self.target.setItem(key, str(value))\n\n    def __delitem__(self, key):\n        \"\"\"\n        Deletes the item for a given key from the storage.\n\n        Args:\n            key (str): The key for the item to delete.\n\n        Raises:\n            KeyError: If the key does not exist in the storage.\n        \"\"\"\n        if self.target.getItem(key) is None:\n            raise KeyError(key)\n        self.target.removeItem(key)\n\n    def __contains__(self, key):\n        \"\"\"\n        Checks if a key exists in the storage.\n\n        Args:\n            key (str): The key to check.\n\n        Returns:\n            bool: True if the key exists, False otherwise.\n        \"\"\"\n        return not self.target.getItem(key) is None\n\n    def __len__(self):\n        \"\"\"\n        Returns the number of items in the storage.\n\n        Returns:\n            int: The number of items in the storage.\n        \"\"\"\n        return self.target.length\n\n    def __iter__(self):\n        \"\"\"\n        Returns an iterator over the keys in the storage.\n\n        Returns:\n            iterator: An iterator over the keys.\n        \"\"\"\n        return iter(self.keys())\n\n    def items(self):\n        \"\"\"\n        Returns an iterator over the (key, value) pairs in the storage.\n\n        Yields:\n            tuple: (key, value) pairs in the storage.\n        \"\"\"\n        for item in Object.entries(self.target):\n            yield item[0], item[1]\n\n    def keys(self):\n        \"\"\"\n        Returns a list of keys in the storage.\n\n        Returns:\n            list: A list of keys.\n        \"\"\"\n        return list(Object.keys(self.target))\n\n    def get(self, key, default=None):\n        \"\"\"\n        Retrieves the value for a given key, returning a default value if the key does not exist.\n\n        Args:\n            key (str): The key for the item to retrieve.\n            default: The default value to return if the key does not exist.\n\n        Returns:\n            The value associated with the key, or the default value.\n        \"\"\"\n        value = self.target.getItem(key)\n        if value is None:\n            return default\n        else:\n            return value\n\n    def clear(self):\n        \"\"\"\n        Clears all items from the storage.\n        \"\"\"\n        self.target.clear()\n\n    def copy(self):\n        \"\"\"\n        Returns a copy of the storage as a dictionary.\n\n        Returns:\n            dict: A dictionary containing all items in the storage.\n        \"\"\"\n        return dict(self.items())\n\n    def pop(self, key, default=NoDefault):\n        \"\"\"\n        Removes the item with the given key from the storage and returns its value.\n\n        Args:\n            key (str): The key for the item to remove.\n            default: The default value to return if the key does not exist.\n\n        Returns:\n            The value associated with the key, or the default value.\n\n        Raises:\n            KeyError: If the key does not exist and no default value is provided.\n        \"\"\"\n        value = self.target.getItem(key)\n        if value is None and default is self.NoDefault:\n            raise KeyError(key)\n        else:\n            self.target.removeItem(key)\n            return value\n\n    def popitem(self):\n        \"\"\"\n        Not implemented. Raises NotImplementedError.\n\n        Raises:\n            NotImplementedError: Always raised as the method is not implemented.\n        \"\"\"\n        raise NotImplementedError(\"popitem not implemented\")\n\n    def reversed(self):\n        \"\"\"\n        Not implemented. Raises NotImplementedError.\n\n        Raises:\n            NotImplementedError: Always raised as the method is not implemented.\n        \"\"\"\n        raise NotImplementedError(\"reversed not implemented\")\n\n    def setdefault(self, key, default=None):\n        \"\"\"\n        Sets the value for the key if it does not already exist in the storage.\n\n        Args:\n            key (str): The key for the item.\n            default: The value to set if the key does not exist.\n\n        Returns:\n            The value associated with the key, or the default value.\n        \"\"\"\n        if key in self:\n            return self[key]\n        else:\n            self[key] = default\n            return default\n\n    def update(self, other):\n        \"\"\"\n        Updates the storage with items from another dictionary or iterable of key-value pairs.\n\n        Args:\n            other: A dictionary or iterable of key-value pairs to update the storage with.\n        \"\"\"\n        for k, v in other.items():\n            self[k] = v\n\n    def values(self):\n        \"\"\"\n        Returns a list of values in the storage.\n\n        Returns:\n            list: A list of values.\n        \"\"\"\n        return list(Object.values(self.target))\n\n    def __str__(self):\n        return self.description\n\n    def __repr__(self):\n        return f\"<{self}>\"\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.NoDefault","title":"NoDefault","text":"

    Placeholder class for default values when no default is provided.

    Source code in puepy/storage.py
    class NoDefault:\n    \"\"\"Placeholder class for default values when no default is provided.\"\"\"\n\n    pass\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__contains__","title":"__contains__(key)","text":"

    Checks if a key exists in the storage.

    Parameters:

    Name Type Description Default key str

    The key to check.

    required

    Returns:

    Name Type Description bool

    True if the key exists, False otherwise.

    Source code in puepy/storage.py
    def __contains__(self, key):\n    \"\"\"\n    Checks if a key exists in the storage.\n\n    Args:\n        key (str): The key to check.\n\n    Returns:\n        bool: True if the key exists, False otherwise.\n    \"\"\"\n    return not self.target.getItem(key) is None\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__delitem__","title":"__delitem__(key)","text":"

    Deletes the item for a given key from the storage.

    Parameters:

    Name Type Description Default key str

    The key for the item to delete.

    required

    Raises:

    Type Description KeyError

    If the key does not exist in the storage.

    Source code in puepy/storage.py
    def __delitem__(self, key):\n    \"\"\"\n    Deletes the item for a given key from the storage.\n\n    Args:\n        key (str): The key for the item to delete.\n\n    Raises:\n        KeyError: If the key does not exist in the storage.\n    \"\"\"\n    if self.target.getItem(key) is None:\n        raise KeyError(key)\n    self.target.removeItem(key)\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__getitem__","title":"__getitem__(key)","text":"

    Retrieves the value for a given key from the storage.

    Parameters:

    Name Type Description Default key str

    The key for the item to retrieve.

    required

    Returns:

    Type Description

    The value associated with the key.

    Raises:

    Type Description KeyError

    If the key does not exist in the storage.

    Source code in puepy/storage.py
    def __getitem__(self, key):\n    \"\"\"\n    Retrieves the value for a given key from the storage.\n\n    Args:\n        key (str): The key for the item to retrieve.\n\n    Returns:\n        The value associated with the key.\n\n    Raises:\n        KeyError: If the key does not exist in the storage.\n    \"\"\"\n    value = self.target.getItem(key)\n    if value is None:\n        raise KeyError(key)\n    return value\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__init__","title":"__init__(target, description)","text":"

    Initializes the BrowserStorage instance.

    Parameters:

    Name Type Description Default target

    The browser storage object.

    required description str

    Description of the storage instance.

    required Source code in puepy/storage.py
    def __init__(self, target, description):\n    \"\"\"\n    Initializes the BrowserStorage instance.\n\n    Args:\n        target: The browser storage object.\n        description (str): Description of the storage instance.\n    \"\"\"\n    self.target = target\n    self.description = description\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__iter__","title":"__iter__()","text":"

    Returns an iterator over the keys in the storage.

    Returns:

    Name Type Description iterator

    An iterator over the keys.

    Source code in puepy/storage.py
    def __iter__(self):\n    \"\"\"\n    Returns an iterator over the keys in the storage.\n\n    Returns:\n        iterator: An iterator over the keys.\n    \"\"\"\n    return iter(self.keys())\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__len__","title":"__len__()","text":"

    Returns the number of items in the storage.

    Returns:

    Name Type Description int

    The number of items in the storage.

    Source code in puepy/storage.py
    def __len__(self):\n    \"\"\"\n    Returns the number of items in the storage.\n\n    Returns:\n        int: The number of items in the storage.\n    \"\"\"\n    return self.target.length\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__setitem__","title":"__setitem__(key, value)","text":"

    Sets the value for a given key in the storage.

    Parameters:

    Name Type Description Default key str

    The key for the item to set.

    required value

    The value to associate with the key.

    required Source code in puepy/storage.py
    def __setitem__(self, key, value):\n    \"\"\"\n    Sets the value for a given key in the storage.\n\n    Args:\n        key (str): The key for the item to set.\n        value: The value to associate with the key.\n    \"\"\"\n    self.target.setItem(key, str(value))\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.clear","title":"clear()","text":"

    Clears all items from the storage.

    Source code in puepy/storage.py
    def clear(self):\n    \"\"\"\n    Clears all items from the storage.\n    \"\"\"\n    self.target.clear()\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.copy","title":"copy()","text":"

    Returns a copy of the storage as a dictionary.

    Returns:

    Name Type Description dict

    A dictionary containing all items in the storage.

    Source code in puepy/storage.py
    def copy(self):\n    \"\"\"\n    Returns a copy of the storage as a dictionary.\n\n    Returns:\n        dict: A dictionary containing all items in the storage.\n    \"\"\"\n    return dict(self.items())\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.get","title":"get(key, default=None)","text":"

    Retrieves the value for a given key, returning a default value if the key does not exist.

    Parameters:

    Name Type Description Default key str

    The key for the item to retrieve.

    required default

    The default value to return if the key does not exist.

    None

    Returns:

    Type Description

    The value associated with the key, or the default value.

    Source code in puepy/storage.py
    def get(self, key, default=None):\n    \"\"\"\n    Retrieves the value for a given key, returning a default value if the key does not exist.\n\n    Args:\n        key (str): The key for the item to retrieve.\n        default: The default value to return if the key does not exist.\n\n    Returns:\n        The value associated with the key, or the default value.\n    \"\"\"\n    value = self.target.getItem(key)\n    if value is None:\n        return default\n    else:\n        return value\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.items","title":"items()","text":"

    Returns an iterator over the (key, value) pairs in the storage.

    Yields:

    Name Type Description tuple

    (key, value) pairs in the storage.

    Source code in puepy/storage.py
    def items(self):\n    \"\"\"\n    Returns an iterator over the (key, value) pairs in the storage.\n\n    Yields:\n        tuple: (key, value) pairs in the storage.\n    \"\"\"\n    for item in Object.entries(self.target):\n        yield item[0], item[1]\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.keys","title":"keys()","text":"

    Returns a list of keys in the storage.

    Returns:

    Name Type Description list

    A list of keys.

    Source code in puepy/storage.py
    def keys(self):\n    \"\"\"\n    Returns a list of keys in the storage.\n\n    Returns:\n        list: A list of keys.\n    \"\"\"\n    return list(Object.keys(self.target))\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.pop","title":"pop(key, default=NoDefault)","text":"

    Removes the item with the given key from the storage and returns its value.

    Parameters:

    Name Type Description Default key str

    The key for the item to remove.

    required default

    The default value to return if the key does not exist.

    NoDefault

    Returns:

    Type Description

    The value associated with the key, or the default value.

    Raises:

    Type Description KeyError

    If the key does not exist and no default value is provided.

    Source code in puepy/storage.py
    def pop(self, key, default=NoDefault):\n    \"\"\"\n    Removes the item with the given key from the storage and returns its value.\n\n    Args:\n        key (str): The key for the item to remove.\n        default: The default value to return if the key does not exist.\n\n    Returns:\n        The value associated with the key, or the default value.\n\n    Raises:\n        KeyError: If the key does not exist and no default value is provided.\n    \"\"\"\n    value = self.target.getItem(key)\n    if value is None and default is self.NoDefault:\n        raise KeyError(key)\n    else:\n        self.target.removeItem(key)\n        return value\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.popitem","title":"popitem()","text":"

    Not implemented. Raises NotImplementedError.

    Raises:

    Type Description NotImplementedError

    Always raised as the method is not implemented.

    Source code in puepy/storage.py
    def popitem(self):\n    \"\"\"\n    Not implemented. Raises NotImplementedError.\n\n    Raises:\n        NotImplementedError: Always raised as the method is not implemented.\n    \"\"\"\n    raise NotImplementedError(\"popitem not implemented\")\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.reversed","title":"reversed()","text":"

    Not implemented. Raises NotImplementedError.

    Raises:

    Type Description NotImplementedError

    Always raised as the method is not implemented.

    Source code in puepy/storage.py
    def reversed(self):\n    \"\"\"\n    Not implemented. Raises NotImplementedError.\n\n    Raises:\n        NotImplementedError: Always raised as the method is not implemented.\n    \"\"\"\n    raise NotImplementedError(\"reversed not implemented\")\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.setdefault","title":"setdefault(key, default=None)","text":"

    Sets the value for the key if it does not already exist in the storage.

    Parameters:

    Name Type Description Default key str

    The key for the item.

    required default

    The value to set if the key does not exist.

    None

    Returns:

    Type Description

    The value associated with the key, or the default value.

    Source code in puepy/storage.py
    def setdefault(self, key, default=None):\n    \"\"\"\n    Sets the value for the key if it does not already exist in the storage.\n\n    Args:\n        key (str): The key for the item.\n        default: The value to set if the key does not exist.\n\n    Returns:\n        The value associated with the key, or the default value.\n    \"\"\"\n    if key in self:\n        return self[key]\n    else:\n        self[key] = default\n        return default\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.update","title":"update(other)","text":"

    Updates the storage with items from another dictionary or iterable of key-value pairs.

    Parameters:

    Name Type Description Default other

    A dictionary or iterable of key-value pairs to update the storage with.

    required Source code in puepy/storage.py
    def update(self, other):\n    \"\"\"\n    Updates the storage with items from another dictionary or iterable of key-value pairs.\n\n    Args:\n        other: A dictionary or iterable of key-value pairs to update the storage with.\n    \"\"\"\n    for k, v in other.items():\n        self[k] = v\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.values","title":"values()","text":"

    Returns a list of values in the storage.

    Returns:

    Name Type Description list

    A list of values.

    Source code in puepy/storage.py
    def values(self):\n    \"\"\"\n    Returns a list of values in the storage.\n\n    Returns:\n        list: A list of values.\n    \"\"\"\n    return list(Object.values(self.target))\n
    "},{"location":"reference/tag/","title":"puepy.core.Tag","text":"

    Tags should not be created directly

    In your populate() method, call t.tag_name() to create a tag. There's no reason an application develop should directly instanciate a tag instance and doing so is not supported.

    The most basic building block of a PuePy app. A Tag is a single HTML element. This is also the base class of Component, which is then the base class of Page.

    Attributes:

    Name Type Description default_classes list

    Default classes for the tag.

    default_attrs dict

    Default attributes for the tag.

    default_role str

    Default role for the tag.

    page Page

    The page the tag is on.

    router Router or None

    The router the application is using, if any.

    parent Tag

    The parent tag, component, or page.

    application Application

    The application instance.

    element

    The rendered element on the DOM. Raises ElementNotInDom if not found.

    children list

    The children of the tag.

    refs dict

    The refs of the tag.

    tag_name str

    The name of the tag.

    ref str

    The reference of the tag.

    Source code in puepy/core.py
    class Tag:\n    \"\"\"\n    The most basic building block of a PuePy app. A Tag is a single HTML element. This is also the base class of\n    `Component`, which is then the base class of `Page`.\n\n    Attributes:\n        default_classes (list): Default classes for the tag.\n        default_attrs (dict): Default attributes for the tag.\n        default_role (str): Default role for the tag.\n        page (Page): The page the tag is on.\n        router (Router or None): The router the application is using, if any.\n        parent (Tag): The parent tag, component, or page.\n        application (Application): The application instance.\n        element: The rendered element on the DOM. Raises ElementNotInDom if not found.\n        children (list): The children of the tag.\n        refs (dict): The refs of the tag.\n        tag_name (str): The name of the tag.\n        ref (str): The reference of the tag.\n    \"\"\"\n\n    stack = []\n    population_stack = []\n    origin_stack = [[]]\n    component_stack = []\n    default_classes = []\n    default_attrs = {}\n    default_role = None\n\n    document = document\n\n    # noinspection t\n    def __init__(\n        self,\n        tag_name,\n        ref,\n        page: \"Page\" = None,\n        parent=None,\n        parent_component=None,\n        origin=None,\n        children=None,\n        **kwargs,\n    ):\n        # Kept so we can garbage collect them later\n        self._added_event_listeners = []\n\n        # Ones manually added, which we persist when reconfigured\n        self._manually_added_event_listeners = {}\n\n        # The rendered element\n        self._rendered_element = None\n\n        # Child nodes and origin refs\n        self.children = []\n        self.refs = {}\n\n        self.tag_name = tag_name\n        self.ref = ref\n\n        # Attrs that webcomponents create that we need to preserve\n        self._retained_attrs = {}\n\n        # Add any children passed to constructor\n        if children:\n            self.add(*children)\n\n        # Configure self._page\n        if isinstance(page, Page):\n            self._page = page\n        elif isinstance(self, Page):\n            self._page = self\n        elif page:\n            raise Exception(f\"Unknown page type {type(page)}\")\n        else:\n            raise Exception(\"No page passed\")\n\n        if \"id\" in kwargs:\n            self._element_id = kwargs[\"id\"]\n        elif self._page and self._page.application:\n            self._element_id = self._page.application.element_id_generator.get_id_for_element(self)\n        else:\n            self._element_id = f\"ppauto-{id(self)}\"\n\n        if isinstance(parent, Tag):\n            self.parent = parent\n            parent.add(self)\n        elif parent:\n            raise Exception(f\"Unknown parent type {type(parent)}: {repr(parent)}\")\n        else:\n            self.parent = None\n\n        if isinstance(parent_component, Component):\n            self.parent_component = parent_component\n        elif parent_component:\n            raise Exception(f\"Unknown parent_component type {type(parent_component)}: {repr(parent_component)}\")\n        else:\n            self.parent_component = None\n\n        self.origin = origin\n        self._children_generated = False\n\n        self._configure(kwargs)\n\n    def __del__(self):\n        if not is_server_side:\n            while self._added_event_listeners:\n                remove_event_listener(*self._added_event_listeners.pop())\n\n    @property\n    def application(self):\n        return self._page._application\n\n    def _configure(self, kwargs):\n        self._kwarg_event_listeners = _extract_event_handlers(kwargs)\n        self._handle_bind(kwargs)\n        self._handle_attrs(kwargs)\n\n    def _handle_bind(self, kwargs):\n        if \"bind\" in kwargs:\n            self.bind = kwargs.pop(\"bind\")\n            input_type = kwargs.get(\"type\")\n            tag_name = self.tag_name.lower()\n\n            if \"value\" in kwargs and not (tag_name == \"input\" and input_type == \"radio\"):\n                raise Exception(\"Cannot specify both 'bind' and 'value'\")\n\n        else:\n            self.bind = None\n\n    def _handle_attrs(self, kwargs):\n        self.attrs = self._retained_attrs.copy()\n        for k, v in kwargs.items():\n            if hasattr(self, f\"set_{k}\"):\n                getattr(self, f\"set_{k}\")(v)\n            else:\n                self.attrs[k] = v\n\n    def populate(self):\n        \"\"\"To be overwritten by subclasses, this method will define the composition of the element\"\"\"\n        pass\n\n    def precheck(self):\n        \"\"\"\n        Before doing too much work, decide whether rendering this tag should raise an error or not. This may be useful,\n        especially on a Page, to check if the user is authorized to view the page, for example:\n\n        Examples:\n            ``` py\n            def precheck(self):\n                if not self.application.state[\"authenticated_user\"]:\n                    raise exceptions.Unauthorized()\n            ```\n        \"\"\"\n        pass\n\n    def generate_children(self):\n        \"\"\"\n        Runs populate, but first adds self to self.population_stack, and removes it after populate runs.\n\n        That way, as populate is executed, self.population_stack can be used to figure out what the innermost populate()\n        method is being run and thus, where to send bind= parameters.\n        \"\"\"\n        self.origin_stack.append([])\n        self._refs_pending_removal = self.refs.copy()\n        self.refs = {}\n        self.population_stack.append(self)\n        try:\n            self.precheck()\n            self.populate()\n        finally:\n            self.population_stack.pop()\n            self.origin_stack.pop()\n\n    def render(self):\n        attrs = self.get_default_attrs()\n        attrs.update(self.attrs)\n\n        element = self._create_element(attrs)\n\n        self._render_onto(element, attrs)\n        self.post_render(element)\n        return element\n\n    def _create_element(self, attrs):\n        if \"xmlns\" in attrs:\n            element = self.document.createElementNS(attrs.get(\"xmlns\"), self.tag_name)\n        else:\n            element = self.document.createElement(self.tag_name)\n\n        element.setAttribute(\"id\", self.element_id)\n        if is_server_side:\n            element.setIdAttribute(\"id\")\n\n        self.configure_element(element)\n\n        return element\n\n    def configure_element(self, element):\n        pass\n\n    def post_render(self, element):\n        pass\n\n    @property\n    def element_id(self):\n        return self._element_id\n\n    @property\n    def element(self):\n        el = self.document.getElementById(self.element_id)\n        if el:\n            return el\n        else:\n            raise ElementNotInDom(self.element_id)\n\n    # noinspection t\n    def _render_onto(self, element, attrs):\n        self._rendered_element = element\n\n        # Handle classes\n        classes = self.get_render_classes(attrs)\n\n        if classes:\n            # element.className = \" \".join(classes)\n            element.setAttribute(\"class\", \" \".join(classes))\n\n        # Add attributes\n        for key, value in attrs.items():\n            if key not in (\"class_name\", \"classes\", \"class\"):\n                if hasattr(self, f\"handle_{key}_attr\"):\n                    getattr(self, f\"handle_{key}_attr\")(element, value)\n                else:\n                    if key.endswith(\"_\"):\n                        attr = key[:-1]\n                    else:\n                        attr = key\n                    attr = attr.replace(\"_\", \"-\")\n\n                    if isinstance(value, bool) or value is None:\n                        if value:\n                            element.setAttribute(attr, attr)\n                    elif isinstance(value, (str, int, float)):\n                        element.setAttribute(attr, value)\n                    else:\n                        element.setAttribute(attr, str(value))\n\n        if \"role\" not in attrs and self.default_role:\n            element.setAttribute(\"role\", self.default_role)\n\n        # Add event handlers\n        self._add_listeners(element, self._kwarg_event_listeners)\n        self._add_listeners(element, self._manually_added_event_listeners)\n\n        # Add bind\n        if self.bind and self.origin:\n            input_type = _element_input_type(element)\n\n            if type(self.bind) in [list, tuple]:\n                value = self.origin.state\n                for key in self.bind:\n                    value = value[key]\n            else:\n                value = self.origin.state[self.bind]\n\n            if input_type == \"checkbox\":\n                if is_server_side and value:\n                    element.setAttribute(\"checked\", value)\n                else:\n                    element.checked = bool(value)\n                    element.setAttribute(\"checked\", value)\n                event_type = \"change\"\n            elif input_type == \"radio\":\n                is_checked = value == element.value\n                if is_server_side and is_checked:\n                    element.setAttribute(\"checked\", is_checked)\n                else:\n                    element.checked = is_checked\n                    element.setAttribute(\"checked\", is_checked)\n                event_type = \"change\"\n            else:\n                if is_server_side:\n                    element.setAttribute(\"value\", value)\n                else:\n                    element.value = value\n                    element.setAttribute(\"value\", value)\n                event_type = \"input\"\n            self._add_event_listener(element, event_type, self.on_bind_input)\n        elif self.bind:\n            raise Exception(\"Cannot specify bind a valid parent component\")\n\n        self.render_children(element)\n\n    def _add_listeners(self, element, listeners):\n        for key, value in listeners.items():\n            key = key.replace(\"_\", \"-\")\n            if isinstance(value, (list, tuple)):\n                for handler in value:\n                    self._add_event_listener(element, key, handler)\n            else:\n                self._add_event_listener(element, key, value)\n\n    def render_children(self, element):\n        for child in self.children:\n            if isinstance(child, Slot):\n                if child.children:  # If slots don't have any children, don't bother.\n                    element.appendChild(child.render())\n            elif isinstance(child, Tag):\n                element.appendChild(child.render())\n            elif isinstance(child, html):\n                element.insertAdjacentHTML(\"beforeend\", str(child))\n            elif isinstance(child, str):\n                element.appendChild(self.document.createTextNode(child))\n            elif child is None:\n                pass\n            elif getattr(child, \"nodeType\", None) is not None:\n                # DOM element\n                element.appendChild(child)\n            else:\n                self.render_unknown_child(element, child)\n\n    def render_unknown_child(self, element, child):\n        \"\"\"\n        Called when the child is not a Tag, Slot, or html. By default, it raises an error.\n        \"\"\"\n        raise Exception(f\"Unknown child type {type(child)} onto {self}\")\n\n    def get_render_classes(self, attrs):\n        class_names, python_css_classes = merge_classes(\n            set(self.get_default_classes()),\n            attrs.pop(\"class_name\", []),\n            attrs.pop(\"classes\", []),\n            attrs.pop(\"class\", []),\n        )\n        self.page.python_css_classes.update(python_css_classes)\n        return class_names\n\n    def get_default_classes(self):\n        \"\"\"\n        Returns a shallow copy of the default_classes list.\n\n        This could be overridden by subclasses to provide a different default_classes list.\n\n        Returns:\n            (list): A shallow copy of the default_classes list.\n        \"\"\"\n        return self.default_classes.copy()\n\n    def get_default_attrs(self):\n        return self.default_attrs.copy()\n\n    def _add_event_listener(self, element, event, listener):\n        \"\"\"\n        Just an internal wrapper around add_event_listener (JS function) that keeps track of what we added, so\n        we can garbage collect it later.\n\n        Should probably not be used outside this class.\n        \"\"\"\n        self._added_event_listeners.append((element, event, listener))\n        if not is_server_side:\n            add_event_listener(element, event, listener)\n\n    def add_event_listener(self, event, handler):\n        \"\"\"\n        Add an event listener for a given event.\n\n        Args:\n            event (str): The name of the event to listen for.\n            handler (function): The function to be executed when the event occurs.\n\n        \"\"\"\n        if event not in self._manually_added_event_listeners:\n            self._manually_added_event_listeners[event] = handler\n        else:\n            existing_handler = self._manually_added_event_listeners[event]\n            if isinstance(existing_handler, (list, tuple)):\n                self._manually_added_event_listeners[event] = [existing_handler] + list(handler)\n            else:\n                self._manually_added_event_listeners[event] = [existing_handler, handler]\n        if self._rendered_element:\n            self._add_event_listener(self._rendered_element, event, handler)\n\n    def mount(self, selector_or_element):\n        self.update_title()\n        if not self._children_generated:\n            with self:\n                self.generate_children()\n\n        if isinstance(selector_or_element, str):\n            element = self.document.querySelector(selector_or_element)\n        else:\n            element = selector_or_element\n\n        if not element:\n            raise RuntimeError(f\"Element {selector_or_element} not found\")\n\n        element.innerHTML = \"\"\n        element.appendChild(self.render())\n        self.recursive_call(\"on_ready\")\n        self.add_python_css_classes()\n\n    def add_python_css_classes(self):\n        \"\"\"\n        This is only done at the page level.\n        \"\"\"\n        pass\n\n    def recursive_call(self, method, *args, **kwargs):\n        \"\"\"\n        Recursively call a specified method on all child Tag objects.\n\n        Args:\n            method (str): The name of the method to be called on each Tag object.\n            *args: Optional arguments to be passed to the method.\n            **kwargs: Optional keyword arguments to be passed to the method.\n        \"\"\"\n        for child in self.children:\n            if isinstance(child, Tag):\n                child.recursive_call(method, *args, **kwargs)\n        getattr(self, method)(*args, **kwargs)\n\n    def on_ready(self):\n        pass\n\n    def _retain_implicit_attrs(self):\n        \"\"\"\n        Retain attributes set elsewhere\n        \"\"\"\n        for attr in self.element.attributes:\n            if attr.name not in self.attrs and attr.name != \"id\":\n                self._retained_attrs[attr.name] = attr.value\n\n    def on_redraw(self):\n        pass\n\n    def on_bind_input(self, event):\n        input_type = _element_input_type(event.target)\n        if input_type == \"checkbox\":\n            self.set_bind_value(self.bind, event.target.checked)\n        elif input_type == \"radio\":\n            if event.target.checked:\n                self.set_bind_value(self.bind, event.target.value)\n        elif input_type == \"number\":\n            value = event.target.value\n            try:\n                if \".\" in str(value):\n                    value = float(value)\n                else:\n                    value = int(value)\n            except (ValueError, TypeError):\n                pass\n            self.set_bind_value(self.bind, value)\n        else:\n            self.set_bind_value(self.bind, event.target.value)\n\n    def set_bind_value(self, bind, value):\n        if type(bind) in (list, tuple):\n            nested_dict = self.origin.state\n            for key in bind[:-1]:\n                nested_dict = nested_dict[key]\n            with self.origin.state.mutate(bind[0]):\n                nested_dict[bind[-1]] = value\n        else:\n            self.origin.state[self.bind] = value\n\n    @property\n    def page(self):\n        if self._page:\n            return self._page\n        elif isinstance(self, Page):\n            return self\n\n    @property\n    def router(self):\n        if self.application:\n            return self.application.router\n\n    @property\n    def parent(self):\n        return self._parent\n\n    @parent.setter\n    def parent(self, new_parent):\n        existing_parent = getattr(self, \"_parent\", None)\n        if new_parent == existing_parent:\n            if new_parent and self not in new_parent.children:\n                existing_parent.children.append(self)\n            return\n\n        if existing_parent and self in existing_parent.children:\n            existing_parent.children.remove(self)\n        if new_parent and self not in new_parent.children:\n            new_parent.children.append(self)\n\n        self._parent = new_parent\n\n    def add(self, *children):\n        for child in children:\n            if isinstance(child, Tag):\n                child.parent = self\n            else:\n                self.children.append(child)\n\n    def redraw(self):\n        if self in self.page.redraw_list:\n            self.page.redraw_list.remove(self)\n\n        try:\n            element = self.element\n        except ElementNotInDom:\n            return\n\n        if is_server_side:\n            old_active_element_id = None\n        else:\n            old_active_element_id = self.document.activeElement.id if self.document.activeElement else None\n\n            self.recursive_call(\"_retain_implicit_attrs\")\n\n        self.children = []\n\n        attrs = self.get_default_attrs()\n        attrs.update(self.attrs)\n\n        self.update_title()\n        with self:\n            self.generate_children()\n\n        staging_element = self._create_element(attrs)\n\n        self._render_onto(staging_element, attrs)\n\n        patch_dom_element(staging_element, element)\n\n        if old_active_element_id is not None:\n            el = self.document.getElementById(old_active_element_id)\n            if el:\n                el.focus()\n\n        self.recursive_call(\"on_redraw\")\n\n    def trigger_event(self, event, detail=None, **kwargs):\n        \"\"\"\n                Triggers an event to be consumed by code using this class.\n\n                Args:\n                    event (str): The name of the event to trigger. If the event name contains underscores, a warning message is printed suggesting to use dashes instead.\n                    detail (dict, optional): Additional data to be sent with the event. This should be a dictionary where the keys and values will be converted to JavaScript objects.\n                    **kwargs: Additional keyword arguments. These arguments are not used in the implementation of the method and are ignored.\n        \u00df\"\"\"\n        if \"_\" in event:\n            print(\"Triggering event with underscores. Did you mean dashes?: \", event)\n\n        # noinspection PyUnresolvedReferences\n        from pyscript.ffi import to_js\n\n        # noinspection PyUnresolvedReferences\n        from js import Object, Map\n\n        if detail:\n            event_object = to_js({\"detail\": Map.new(Object.entries(to_js(detail)))})\n        else:\n            event_object = to_js({})\n\n        self.element.dispatchEvent(CustomEvent.new(event, event_object))\n\n    def update_title(self):\n        \"\"\"\n        To be overridden by subclasses (usually pages), this method should update the Window title as needed.\n\n        Called on mounting or redraw.\n        \"\"\"\n        pass\n\n    def __enter__(self):\n        self.stack.append(self)\n        self.origin_stack[0].append(self)\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.stack.pop()\n        self.origin_stack[0].pop()\n        return False\n\n    def __str__(self):\n        return self.tag_name\n\n    def __repr__(self):\n        return f\"<{self} ({id(self)})>\"\n
    "},{"location":"reference/tag/#puepy.core.Tag._add_event_listener","title":"_add_event_listener(element, event, listener)","text":"

    Just an internal wrapper around add_event_listener (JS function) that keeps track of what we added, so we can garbage collect it later.

    Should probably not be used outside this class.

    Source code in puepy/core.py
    def _add_event_listener(self, element, event, listener):\n    \"\"\"\n    Just an internal wrapper around add_event_listener (JS function) that keeps track of what we added, so\n    we can garbage collect it later.\n\n    Should probably not be used outside this class.\n    \"\"\"\n    self._added_event_listeners.append((element, event, listener))\n    if not is_server_side:\n        add_event_listener(element, event, listener)\n
    "},{"location":"reference/tag/#puepy.core.Tag._retain_implicit_attrs","title":"_retain_implicit_attrs()","text":"

    Retain attributes set elsewhere

    Source code in puepy/core.py
    def _retain_implicit_attrs(self):\n    \"\"\"\n    Retain attributes set elsewhere\n    \"\"\"\n    for attr in self.element.attributes:\n        if attr.name not in self.attrs and attr.name != \"id\":\n            self._retained_attrs[attr.name] = attr.value\n
    "},{"location":"reference/tag/#puepy.core.Tag.add_event_listener","title":"add_event_listener(event, handler)","text":"

    Add an event listener for a given event.

    Parameters:

    Name Type Description Default event str

    The name of the event to listen for.

    required handler function

    The function to be executed when the event occurs.

    required Source code in puepy/core.py
    def add_event_listener(self, event, handler):\n    \"\"\"\n    Add an event listener for a given event.\n\n    Args:\n        event (str): The name of the event to listen for.\n        handler (function): The function to be executed when the event occurs.\n\n    \"\"\"\n    if event not in self._manually_added_event_listeners:\n        self._manually_added_event_listeners[event] = handler\n    else:\n        existing_handler = self._manually_added_event_listeners[event]\n        if isinstance(existing_handler, (list, tuple)):\n            self._manually_added_event_listeners[event] = [existing_handler] + list(handler)\n        else:\n            self._manually_added_event_listeners[event] = [existing_handler, handler]\n    if self._rendered_element:\n        self._add_event_listener(self._rendered_element, event, handler)\n
    "},{"location":"reference/tag/#puepy.core.Tag.add_python_css_classes","title":"add_python_css_classes()","text":"

    This is only done at the page level.

    Source code in puepy/core.py
    def add_python_css_classes(self):\n    \"\"\"\n    This is only done at the page level.\n    \"\"\"\n    pass\n
    "},{"location":"reference/tag/#puepy.core.Tag.generate_children","title":"generate_children()","text":"

    Runs populate, but first adds self to self.population_stack, and removes it after populate runs.

    That way, as populate is executed, self.population_stack can be used to figure out what the innermost populate() method is being run and thus, where to send bind= parameters.

    Source code in puepy/core.py
    def generate_children(self):\n    \"\"\"\n    Runs populate, but first adds self to self.population_stack, and removes it after populate runs.\n\n    That way, as populate is executed, self.population_stack can be used to figure out what the innermost populate()\n    method is being run and thus, where to send bind= parameters.\n    \"\"\"\n    self.origin_stack.append([])\n    self._refs_pending_removal = self.refs.copy()\n    self.refs = {}\n    self.population_stack.append(self)\n    try:\n        self.precheck()\n        self.populate()\n    finally:\n        self.population_stack.pop()\n        self.origin_stack.pop()\n
    "},{"location":"reference/tag/#puepy.core.Tag.get_default_classes","title":"get_default_classes()","text":"

    Returns a shallow copy of the default_classes list.

    This could be overridden by subclasses to provide a different default_classes list.

    Returns:

    Type Description list

    A shallow copy of the default_classes list.

    Source code in puepy/core.py
    def get_default_classes(self):\n    \"\"\"\n    Returns a shallow copy of the default_classes list.\n\n    This could be overridden by subclasses to provide a different default_classes list.\n\n    Returns:\n        (list): A shallow copy of the default_classes list.\n    \"\"\"\n    return self.default_classes.copy()\n
    "},{"location":"reference/tag/#puepy.core.Tag.populate","title":"populate()","text":"

    To be overwritten by subclasses, this method will define the composition of the element

    Source code in puepy/core.py
    def populate(self):\n    \"\"\"To be overwritten by subclasses, this method will define the composition of the element\"\"\"\n    pass\n
    "},{"location":"reference/tag/#puepy.core.Tag.precheck","title":"precheck()","text":"

    Before doing too much work, decide whether rendering this tag should raise an error or not. This may be useful, especially on a Page, to check if the user is authorized to view the page, for example:

    Examples:

    def precheck(self):\n    if not self.application.state[\"authenticated_user\"]:\n        raise exceptions.Unauthorized()\n
    Source code in puepy/core.py
    def precheck(self):\n    \"\"\"\n    Before doing too much work, decide whether rendering this tag should raise an error or not. This may be useful,\n    especially on a Page, to check if the user is authorized to view the page, for example:\n\n    Examples:\n        ``` py\n        def precheck(self):\n            if not self.application.state[\"authenticated_user\"]:\n                raise exceptions.Unauthorized()\n        ```\n    \"\"\"\n    pass\n
    "},{"location":"reference/tag/#puepy.core.Tag.recursive_call","title":"recursive_call(method, *args, **kwargs)","text":"

    Recursively call a specified method on all child Tag objects.

    Parameters:

    Name Type Description Default method str

    The name of the method to be called on each Tag object.

    required *args

    Optional arguments to be passed to the method.

    () **kwargs

    Optional keyword arguments to be passed to the method.

    {} Source code in puepy/core.py
    def recursive_call(self, method, *args, **kwargs):\n    \"\"\"\n    Recursively call a specified method on all child Tag objects.\n\n    Args:\n        method (str): The name of the method to be called on each Tag object.\n        *args: Optional arguments to be passed to the method.\n        **kwargs: Optional keyword arguments to be passed to the method.\n    \"\"\"\n    for child in self.children:\n        if isinstance(child, Tag):\n            child.recursive_call(method, *args, **kwargs)\n    getattr(self, method)(*args, **kwargs)\n
    "},{"location":"reference/tag/#puepy.core.Tag.render_unknown_child","title":"render_unknown_child(element, child)","text":"

    Called when the child is not a Tag, Slot, or html. By default, it raises an error.

    Source code in puepy/core.py
    def render_unknown_child(self, element, child):\n    \"\"\"\n    Called when the child is not a Tag, Slot, or html. By default, it raises an error.\n    \"\"\"\n    raise Exception(f\"Unknown child type {type(child)} onto {self}\")\n
    "},{"location":"reference/tag/#puepy.core.Tag.trigger_event","title":"trigger_event(event, detail=None, **kwargs)","text":"
        Triggers an event to be consumed by code using this class.\n\n    Args:\n        event (str): The name of the event to trigger. If the event name contains underscores, a warning message is printed suggesting to use dashes instead.\n        detail (dict, optional): Additional data to be sent with the event. This should be a dictionary where the keys and values will be converted to JavaScript objects.\n        **kwargs: Additional keyword arguments. These arguments are not used in the implementation of the method and are ignored.\n

    \u00df

    Source code in puepy/core.py
    def trigger_event(self, event, detail=None, **kwargs):\n    \"\"\"\n            Triggers an event to be consumed by code using this class.\n\n            Args:\n                event (str): The name of the event to trigger. If the event name contains underscores, a warning message is printed suggesting to use dashes instead.\n                detail (dict, optional): Additional data to be sent with the event. This should be a dictionary where the keys and values will be converted to JavaScript objects.\n                **kwargs: Additional keyword arguments. These arguments are not used in the implementation of the method and are ignored.\n    \u00df\"\"\"\n    if \"_\" in event:\n        print(\"Triggering event with underscores. Did you mean dashes?: \", event)\n\n    # noinspection PyUnresolvedReferences\n    from pyscript.ffi import to_js\n\n    # noinspection PyUnresolvedReferences\n    from js import Object, Map\n\n    if detail:\n        event_object = to_js({\"detail\": Map.new(Object.entries(to_js(detail)))})\n    else:\n        event_object = to_js({})\n\n    self.element.dispatchEvent(CustomEvent.new(event, event_object))\n
    "},{"location":"reference/tag/#puepy.core.Tag.update_title","title":"update_title()","text":"

    To be overridden by subclasses (usually pages), this method should update the Window title as needed.

    Called on mounting or redraw.

    Source code in puepy/core.py
    def update_title(self):\n    \"\"\"\n    To be overridden by subclasses (usually pages), this method should update the Window title as needed.\n\n    Called on mounting or redraw.\n    \"\"\"\n    pass\n
    "},{"location":"tutorial/00-using-this-tutorial/","title":"Using This Tutorial","text":"

    Each of the examples in this tutorial can be run on PyScript.com, a web environment that lets you write, test, and share PyScript code. Alternatively, you can clone the PuePy git repo and run a live web server with each example included.

    The PyScript.com environment uses the PuePy .whl file, as downloadable from PyPi, while the examples in the git repository are served with PuePy directly from source files on disk.

    "},{"location":"tutorial/00-using-this-tutorial/#using-pyscriptcom","title":"Using PyScript.com","text":"

    Navigate to https://pyscript.com/@kkinder/puepy-tutorial/latest and you are greeted with a list of files on the left, a code editor in the middle, and a running example on the left. Each chapter in the tutorial corresponds with a directory in tutorial folder on the left.

    You can clone the entire examples project and edit it yourself to continue your learning:

    Once cloned you make your own changes and experiment with them in real time.

    "},{"location":"tutorial/00-using-this-tutorial/#editing-locally","title":"Editing locally","text":"

    After cloning puepy on git, you can run the examples using a simple script:

    http://localhost:8000/ show you a list of examples

    As you edit them in the examples folder and reload the window, your changes will be live.

    "},{"location":"tutorial/00-using-this-tutorial/#live-examples","title":"Live Examples","text":"

    Most of the examples you see live in this tutorial include example code running live in a browser like this:

    There, you can see the running example inline with its explanation. You can also edit the code on PyScript.com by navigating to its example folder and cloning the project, as described above.

    "},{"location":"tutorial/01-hello-world/","title":"Hello, World! 0.6.2","text":"

    0.6.2 Let's start with the simplest possible: Hello, World!

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/01_hello_world/index.htmlEdit

    hello_world.pyindex.htmlpyscript.json
    from puepy import Application, Page, t\n\napp = Application()\n\n\n@app.page()\nclass HelloWorldPage(Page):\n    def populate(self):\n        t.h1(\"Hello, World!\")\n\n\napp.mount(\"#app\")\n
    <!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <title>PuePy Hello, World</title>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n  <link rel=\"stylesheet\" href=\"https://pyscript.net/releases/2025.2.2/core.css\">\n  <script type=\"module\" src=\"https://pyscript.net/releases/2025.2.2/core.js\"></script>\n</head>\n<body>\n<div id=\"app\">Loading...</div>\n<script type=\"mpy\" src=\"./hello_world.py\" config=\"../../pyscript.json\"></script>\n</body>\n</html>\n
    {\n  \"name\": \"PuePy Tutorial\",\n  \"debug\": true,\n  \"files\": {},\n  \"js_modules\": {\n    \"main\": {\n      \"https://cdn.jsdelivr.net/npm/morphdom@2.7.2/+esm\": \"morphdom\"\n    }\n  },\n  \"packages\": [\n    \"./puepy-0.6.2-py3-none-any.whl\"\n  ]\n}\n
    "},{"location":"tutorial/01-hello-world/#including-pyscript","title":"Including PyScript","text":"

    Let's start with the HTML. To use PuePy, we include PyScript from its CDN:

    index.html
      <link rel=\"stylesheet\" href=\"https://pyscript.net/releases/2025.2.2/core.css\">\n  <script type=\"module\" src=\"https://pyscript.net/releases/2025.2.2/core.js\"></script>\n

    Then, we include our PyScript config file and also execute our hello_world.py file:

    index.html
    <script type=\"mpy\" src=\"./hello_world.py\" config=\"../../pyscript.json\"></script>\n
    "},{"location":"tutorial/01-hello-world/#pyscript-configuration","title":"PyScript configuration","text":"

    PyScript Configuration

    The official PyScript documentation has more information on PyScript configuration.

    The PyScript configuration must, at minimum, tell PyScript to use PuePy (usually as a package) and include Morphdom, which is a dependency of PuePy.

    "},{"location":"tutorial/01-hello-world/#the-python-code","title":"The Python Code","text":"

    Let's take a look at our Python code which actually renders Hello, World.

    First, we import Application, Page, and t from puepy:

    from puepy import Application, Page, t\n

    To use PuePy, you must always create an Application instance, even if the application only has one page:

    app = Application()\n

    Next, we define a Page and use the t singleton to compose our DOM in the populate() method. Don't worry too much about the details for now; just know that this is how we define pages and components in PuePy:

    @app.page()\nclass HelloWorldPage(Page):\n    def populate(self):\n        t.h1(\"Hello, World!\")\n

    Finally, we tell PuePy where to mount the application. This is where the application will be rendered in the DOM. The #app element was already defined in our HTML file.

    app.mount(\"#app\")\n

    And with that, the page is added to the application, and the application is mounted in the element with id app.

    Watching for Errors

    Use your browser's development console to watch for any errors.

    "},{"location":"tutorial/02-hello-name/","title":"Hello, Name","text":"

    In this chapter, we introduce state and variables by creating a simple form that asks for a name and greets the user.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/02_hello_name/index.htmlEdit

    The html and pyscript configuration are the same as in the previous Hello, World chapter, so we will only study the Python code. Expand the annotations in the code below for a more detail explanation of the changes:

    hello_name.py
    from puepy import Application, Page, t\n\napp = Application()\n\n\n@app.page()\nclass HelloNamePage(Page):\n    def initial(self):\n        return {\"name\": \"\"}  # (1)\n\n    def populate(self):\n        if self.state[\"name\"]:  # (2)\n            t.h1(f\"Hello, {self.state['name']}!\")\n        else:\n            t.h1(f\"Why don't you tell me your name?\")\n\n        with t.div(style=\"margin: 1em\"):\n            t.input(bind=\"name\", placeholder=\"name\", autocomplete=\"off\")  # (3)\n\n\napp.mount(\"#app\")\n
    1. The initial() method defines the page's initial working state. In this case, it returns a dictionary with a single key, name, which is initially an empty string.
    2. We check the value of self.state[\"name\"] and renders different content based on that value.
    3. We define an input element with a bind=\"name\" parameter. This binds the input element to the name key in the page's state. When the input value changes, the state is updated, and the page is re-rendered.
    "},{"location":"tutorial/02-hello-name/#reactivity","title":"Reactivity","text":"

    A page or component's initial state is defined by the initial() method. If implemented, it should return a dictionary, which is then stored as a special reactive dictionary, self.state. As the state is modified, the component redraws, updating the DOM as needed.

    Modifying .state values in-place will not work

    For complex objects like lists and dictionaries, you cannot modify them in-place and expect the component to re-render.

    # THESE WILL NOT WORK:\nself.state[\"my_list\"].append(\"spam\")\nself.state[\"my_dict\"][\"spam\"] = \"eggs\"\n

    This is because PuePy's ReactiveDict cannot detect \"deep\" changes to state automatically. If you are modifying objects in-place, use with self.state.mutate() as a context manager:

    # This will work\nwith self.state.mutate(\"my_list\", \"my_dict\"):\n    self.state[\"my_list\"].append(\"spam\")\n    self.state[\"my_dict\"][\"spam\"] = \"eggs\"\n
    More information on reactivity

    For more information on reactivity in PuePy, see the Reactivity Developer Guide.

    "},{"location":"tutorial/03-events/","title":"Counter","text":"

    In this chapter, we introduce event handlers. Take a look at this demo, with plus and minus buttons that increment and decrement a counter. You may remember it from the pupy.dev homepage.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/03_counter/index.htmlEdit

    In this example, we bind two events to event handlers. Follow along with the annotations in the code below for a more detailed explanation:

    counter.py
    from puepy import Application, Page, t\n\napp = Application()\n\n\n@app.page()\nclass CounterPage(Page):\n    def initial(self):\n        return {\"current_value\": 0}\n\n    def populate(self):\n        with t.div(classes=\"button-box\"):\n            t.button(\"-\", \n                     classes=[\"button\", \"decrement-button\"],\n                     on_click=self.on_decrement_click)  # (1)\n            t.span(str(self.state[\"current_value\"]), classes=\"count\")\n            t.button(\"+\", \n                     classes=\"button increment-button\",\n                     on_click=self.on_increment_click)  # (2)\n\n    def on_decrement_click(self, event):\n        self.state[\"current_value\"] -= 1  # (3)\n\n    def on_increment_click(self, event):\n        self.state[\"current_value\"] += 1  # (4)\n\n\napp.mount(\"#app\")\n
    1. The on_click parameter is passed to the button tag, which binds the on_decrement_click method to the button's click event.
    2. The on_click parameter is passed to the button tag, which binds the on_increment_click method to the button's click event.
    3. The on_decrement_click method decrements the current_value key in the page's state.
    4. The on_increment_click method increments the current_value key in the page's state.

    Tip

    The event parameter sent to event handlers is the same as it is in JavaScript. You can call event.preventDefault() or event.stopPropagation() as needed.

    As before, because we are modifying the state directly, the page will re-render automatically. This is the power of PuePy's reactivity system.

    "},{"location":"tutorial/04-refs/","title":"Using Refs","text":"

    Let's introduce our first bug. Try typing a word in the input box in the demo below.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/04_refs_problem/index.htmlEdit

    Notice that as you type, each time the page redraws, your input loses focus. This is because PuePy doesn't know which elements are supposed to \"match\" the ones from the previous refresh, and the ordering is now different. The original <input> is being discarded each refresh and replaced with a new one.

    Now try the fixed version:

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/04_refs_problem/solution.htmlEdit

    Here's the problem code and the fixed code. Notice the addition of a ref= in the fixed version.

    Problem CodeFixed Code
    @app.page()\nclass RefsProblemPage(Page):\n    def initial(self):\n        return {\"word\": \"\"}\n\n    def populate(self):\n        t.h1(\"Problem: DOM elements are re-created\")\n        if self.state[\"word\"]:\n            for char in self.state[\"word\"]:\n                t.span(char, classes=\"char-box\")\n        with t.div(style=\"margin-top: 1em\"):\n            t.input(bind=\"word\", placeholder=\"Type a word\")\n
    @app.page()\nclass RefsSolutionPage(Page):\n    def initial(self):\n        return {\"word\": \"\"}\n\n    def populate(self):\n        t.h1(\"Solution: Use ref=\")\n        if self.state[\"word\"]:\n            for char in self.state[\"word\"]:\n                t.span(char, classes=\"char-box\")\n        with t.div(style=\"margin-top: 1em\"):\n            t.input(bind=\"word\", placeholder=\"Type a word\", ref=\"enter_word\")\n
    "},{"location":"tutorial/04-refs/#using-refs-to-preserve-elements-between-refreshes","title":"Using refs to preserve elements between refreshes","text":"

    To tell PuePy not to garbage collect an element, but to reuse it between redraws, just give it a ref= parameter. The ref should be unique to the component you're coding: that is, each ref should be unique among all elements created in the populate() method you're writing.

    When PuePy finds an element with a ref, it will reuse that ref if it existed in the last refresh, modifying it with any updated parameters passed to it.

    Using references in your code

    The self.refs dictionary is available to you in your page or component. You can access elements by their ref name, like self.refs[\"enter_word\"].

    "},{"location":"tutorial/05-watchers/","title":"Watchers","text":"

    We've introduced reactivity, but what happens when you want to monitor specific variables for changes? In PuePy, you can use on_<variable>_change methods in your components to watch for changes in specific variables. In the example below, try guessing the number 4:

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/05_watchers/index.htmlEdit

    watchers.py
    @app.page()\nclass WatcherPage(Page):\n    def initial(self):\n        self.winner = 4\n\n        return {\"number\": \"\", \"message\": \"\"}\n\n    def populate(self):\n        t.h1(\"Can you guess a number between 1 and 10?\")\n\n        with t.div(style=\"margin: 1em\"):\n            t.input(bind=\"number\", placeholder=\"Enter a guess\", autocomplete=\"off\", type=\"number\", maxlength=1)\n\n        if self.state[\"message\"]:\n            t.p(self.state[\"message\"])\n\n    def on_number_change(self, event):  # (1)\n        try:\n            if int(self.state[\"number\"]) == self.winner:\n                self.state[\"message\"] = \"You guessed the number!\"\n            else:\n                self.state[\"message\"] = \"Keep trying...\"\n        except (ValueError, TypeError):\n            self.state[\"message\"] = \"\"\n
    1. The function name, on_number_change is automatically registered based on the pattern of on_<variable>_change. The event parameter is passed up from the original JavaScript event that triggered the change.

    The watcher method itself changes the self.state[\"message\"] variable based on the value of self.state[\"number\"]. If the number is equal to the self.winner constant, the message is updated to \"You guessed the number!\" Otherwise, the message is set to \"Keep trying...\". The state is once again changed and the page is re-rendered.

    "},{"location":"tutorial/06-components/","title":"Components","text":"

    Components are a way to encapsulate a piece of UI that can be reused throughout your application. In this example, we'll create a Card component and use it multiple times on a page, each time using slots to fill in content.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/06_components/index.htmlEdit

    Component DefinitionComponent Usage
    @t.component()  # (1)!\nclass Card(Component):  # (2)!\n    props = [\"type\", \"button_text\"]  # (3)!\n\n    card = CssClass(  # (4)!\n        margin=\"1em\",\n        padding=\"1em\",\n        background_color=\"#efefef\",\n        border=\"solid 2px #333\",\n    )\n\n    default_classes = [card]\n\n    type_styles = {\n        \"success\": success,\n        \"warning\": warning,\n        \"error\": error,\n    }\n\n    def populate(self):\n        with t.h2(classes=[self.type_styles[self.type]]):\n            self.insert_slot(\"card-header\")    # (5)!\n        with t.p():\n            self.insert_slot()    # (6)!\n        t.button(self.button_text, on_click=self.on_button_click)\n\n    def on_button_click(self, event):\n        self.trigger_event(\"my-custom-event\",\n            detail={\"type\": self.type})  # (7)!\n
    1. The @t.component() decorator registers the class as a component for use elsewhere.
    2. All components should subclass the puepy.Component class.
    3. The props attribute is a list of properties that can be passed to the component.
    4. Classes can be defined programmatically in Python. Class names are automatically generated for each instance, so they're scoped like Python instances.
    5. default_classes is a list of CSS classes that will be applied to the component by default.
    6. The insert_slot method is used to insert content into a named slot. In this case, we are inserting content into the card-header slot.
    7. Unnamed, or default slots, can be filled by calling insert_slot without a name.
    8. trigger_event is used to trigger a custom event. Notice the detail dictionary. This pattern matches the JavaScript CustomEvent API.
    @app.page()\nclass ComponentPage(Page):\n    def initial(self):\n        return {\"message\": \"\"}\n\n    def populate(self):\n        t.h1(\"Components are useful\")\n\n        with t.card(type=\"success\",  # (1)\n                    on_my_custom_event=self.handle_custom_event) as card:  # (2)\n            with card.slot(\"card-header\"):\n                t(\"Success!\")  # (3)\n            with card.slot():\n                t(\"Your operation worked\")  # (4)\n\n        with t.card(type=\"warning\", on_my_custom_event=self.handle_custom_event) as card:\n            with card.slot(\"card-header\"):\n                t(\"Warning!\")\n            with card.slot():\n                t(\"Your operation may not work\")\n\n        with t.card(type=\"error\", on_my_custom_event=self.handle_custom_event) as card:\n            with card.slot(\"card-header\"):\n                t(\"Failure!\")\n            with card.slot():\n                t(\"Your operation failed\")\n\n        if self.state[\"message\"]:\n            t.p(self.state[\"message\"])\n\n    def handle_custom_event(self, event):  # (5)\n        self.state[\"message\"] = f\"Custom event from card with type {event.detail.get('type')}\"\n
    1. The card component is used with the type prop set to \"success\".
    2. The my-custom-event event is bound to the self.handle_custom_event method.
    3. The content for the card-header slot, as defined in the Card component, is populated with \"Success!\".
    4. The default slot is populated with \"Your operation worked\". Default slots are not named.
    5. The handle_custom_event method is called when the my-custom-event event is triggered.
    "},{"location":"tutorial/06-components/#slots","title":"Slots","text":"

    Slots are a way to pass content into a component. A component can define one or more slots, and the calling code can fill in the slots with content. In the example above, the Card component defines two slots: card-header and the default slot. The calling code fills in the slots by calling card.slot(\"card-header\") and card.slot().

    Defining Slots in a componentFilling Slots in the calling code
    with t.h2():\n    self.insert_slot(\"card-header\")\nwith t.p():\n    self.insert_slot()  # (1)\n
    1. If you don't pass a name, it defaults to the main slot
    with t.card() as card:\n    with card.slot(\"card-header\"):\n        t(\"Success!\")\n    with card.slot():\n        t(\"Your operation worked\")\n

    Consuming Slots

    When consuming components with slots, to populate a slot, you do not call t.slot. You call .slot directly on the component instance provided by the context manager:

    with t.card() as card:\n    with card.slot(\"card-header\"):  # (1)\n        t(\"Success!\")\n
    1. Notice card.slot is called, not t.slot or self.slot.
    More information on components

    For more information on components in PuePy, see the Component Developer Guide.

    "},{"location":"tutorial/07-routing/","title":"Routing","text":"

    For single page apps (SPAs) or even complex pages with internal navigation, PuePy's client-side routing feature renders different pages based on the URL and provides a way of linking between various routes. Use of the router is optional and if no router is installed, the application will always render the default page.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/07_routing/index.htmlEdit

    URL Changes

    In the embedded example above, the \"URL\" does not change because the embedded example is not a full web page. In a full web page, the URL would change to reflect the current page. Try opening the example in a new window to see the URL change.

    Inspired by Flask's simple and elegant routing system, PuePy uses decorators on page classes to define routes and parameters. The router can be configured to use either hash-based or history-based routing. Consider this example's source code:

    routing.py
    from puepy import Application, Page, Component, t\nfrom puepy.router import Router\n\napp = Application()\napp.install_router(Router, link_mode=Router.LINK_MODE_HASH)  # (1)\n\npets = {\n    \"scooby\": {\"name\": \"Scooby-Doo\", \"type\": \"dog\", \"character\": \"fearful\"},\n    \"garfield\": {\"name\": \"Garfield\", \"type\": \"cat\", \"character\": \"lazy\"},\n    \"snoopy\": {\"name\": \"Snoopy\", \"type\": \"dog\", \"character\": \"playful\"},\n}\n\n\n@t.component()\nclass Link(Component):  # (2)\n    props = [\"args\"]\n    enclosing_tag = \"a\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        self.add_event_listener(\"click\", self.on_click)\n\n    def set_href(self, href):\n        if issubclass(href, Page):\n            args = self.args or {}\n            self._resolved_href = self.page.router.reverse(href, **args)\n        else:\n            self._resolved_href = href\n\n        self.attrs[\"href\"] = self._resolved_href\n\n    def on_click(self, event):\n        if (\n            isinstance(self._resolved_href, str)\n            and self._resolved_href[0] in \"#/\"\n            and self.page.router.navigate_to_path(self._resolved_href)\n        ):\n            # A page was found; prevent navigation and navigate to page\n            event.preventDefault()\n\n\n@app.page(\"/pet/<pet_id>\")  # (3)\nclass PetPage(Page):\n    props = [\"pet_id\"]\n\n    def populate(self):\n        pet = pets.get(self.pet_id)\n        t.h1(\"Pet Information\")\n        with t.dl():\n            for k, v in pet.items():\n                t.dt(k)\n                t.dd(v)\n        t.link(\"Back to Homepage\", href=DefaultPage)  # (4)\n\n\n@app.page()\nclass DefaultPage(Page):\n    def populate(self):\n        t.h1(\"PuePy Routing Demo: Pet Listing\")\n        with t.ul():\n            for pet_id, pet_details in pets.items():\n                with t.li():\n                    t.link(pet_details[\"name\"],\n                           href=PetPage,\n                           args={\"pet_id\": pet_id})  # (5)\n\n\napp.mount(\"#app\")\n
    1. The router is installed with the link_mode set to Router.LINK_MODE_HASH. This sets the router to use hash-based routing.
    2. The Link component is a custom component that creates links to other pages. It uses the router to navigate to the specified page.
    3. The PetPage class is decorated with a route. The pet_id parameter is parsed from the URL.
    4. The Link component is used to create a link back to the homepage, as passed by the href parameter.
    5. The Link component is used to create links to each pet's page, passing the pet_id as a parameter.
    "},{"location":"tutorial/07-routing/#installing-the-router","title":"Installing the router","text":"

    The router is installed with the install_router method on the application instance:

    app.install_router(Router, link_mode=Router.LINK_MODE_HASH)\n

    If you wanted to use html5 history mode (see the Router developer guide), you would set link_mode=Router.LINK_MODE_HISTORY.

    "},{"location":"tutorial/07-routing/#the-default-page","title":"The default page","text":"

    The default page is rendered for the \"root\" URL or when no URL is specified. The default page is defined with no path:

    @app.page()\nclass DefaultPage(Page):\n    ...\n
    More information on the router

    For more information on the router, see the Router Developer Guide.

    "},{"location":"tutorial/08-pypi-libraries/","title":"Using PyPi Libraries","text":"

    Let's make use of a PyPi library in our project. In this example, we'll use BeautifulSoup to parse an HTML document and actually generate a PuePy component that would render the same content.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/08_libraries/index.htmlEdit

    Small embedded example

    This example may be more useful in a full browser window. Open in new window

    "},{"location":"tutorial/08-pypi-libraries/#using-full-cpythonpyodide","title":"Using Full CPython/Pyodide","text":"

    To make use of a library like BeautifulSoup, we will configure PuePy to use the full CPython/Pyoide runtime, rather than the more minimal MicroPython runtime. This is done by specifying the runtime in the <script> tag in index.html:

    <script type=\"py\" src=\"./libraries.py\" config=\"./pyscript-bs.json\"></script>\n
    "},{"location":"tutorial/08-pypi-libraries/#requiring-packages-from-pypi","title":"Requiring packages from pypi","text":"

    In pyscript-bs.json, we also must specify that we need BeautifulSoup4. This is done by adding it to the packages section of the config file:

    pyscript-bs.json
    {\n  \"name\": \"PuePy Tutorial\",\n  \"debug\": true,\n  \"packages\": [\n    \"./puepy-0.5.0-py3-none-any.whl\",\n    \"beautifulsoup4\"\n  ],\n  \"js_modules\": {\n    \"main\": {\n      \"https://cdn.jsdelivr.net/npm/morphdom@2.7.2/+esm\": \"morphdom\"\n    }\n  }\n}\n

    The type attribute in the PyScript <script> tag can have two values:

    • mpy: Use the MicroPython runtime
    • py: Use the CPython/Pyodide runtime

    See Also

    See also the runtimes developer guide for more information on runtimes.

    Once the dependencies are specified in the config file, we can import the library in our source file:

    from bs4 import BeautifulSoup, Comment\n
    Full Example Source libraries.py
    import re\nfrom bs4 import BeautifulSoup, Comment\nfrom puepy import Application, Page, t\n\napp = Application()\n\nPYTHON_KEYWORDS = [\n    \"false\",\n    \"none\",\n    \"true\",\n    \"and\",\n    \"as\",\n    \"assert\",\n    \"async\",\n    \"await\",\n    \"break\",\n    \"class\",\n    \"continue\",\n    \"def\",\n    \"del\",\n    \"elif\",\n    \"else\",\n    \"except\",\n    \"finally\",\n    \"for\",\n    \"from\",\n    \"global\",\n    \"if\",\n    \"import\",\n    \"in\",\n    \"is\",\n    \"lambda\",\n    \"nonlocal\",\n    \"not\",\n    \"or\",\n    \"pass\",\n    \"raise\",\n    \"return\",\n    \"try\",\n    \"while\",\n    \"with\",\n    \"yield\",\n]\n\n\nclass TagGenerator:\n    def __init__(self, indentation=4):\n        self.indent_level = 0\n        self.indentation = indentation\n\n    def indent(self):\n        return \" \" * self.indentation * self.indent_level\n\n    def sanitize(self, key):\n        key = re.sub(r\"\\W\", \"_\", key)\n        if not key[0].isalpha():\n            key = f\"_{key}\"\n        if key == \"class\":\n            key = \"classes\"\n        elif key.lower() in PYTHON_KEYWORDS:\n            key = f\"{key}_\"\n        return key\n\n    def generate_tag(self, tag):\n        attr_list = [\n            f\"{self.sanitize(key)}={repr(' '.join(value) if isinstance(value, list) else value)}\"\n            for key, value in tag.attrs.items()\n        ]\n\n        underscores_tag_name = tag.name.replace(\"-\", \"_\")\n\n        sanitized_tag_name = self.sanitize(underscores_tag_name)\n        if sanitized_tag_name != underscores_tag_name:\n            # For the rare case where it really just has to be the original tag\n            attr_list.append(f\"tag={repr(tag.name)}\")\n\n        attributes = \", \".join(attr_list)\n\n        return (\n            f\"{self.indent()}with t.{sanitized_tag_name}({attributes}):\"\n            if tag.contents\n            else f\"{self.indent()}t.{sanitized_tag_name}({attributes})\"\n        )\n\n    def iterate_node(self, node):\n        output = []\n        for child in node.children:\n            if child.name:  # Element\n                output.append(self.generate_tag(child))\n                self.indent_level += 1\n                if child.contents:\n                    output.extend(self.iterate_node(child))\n                self.indent_level -= 1\n            elif isinstance(child, Comment):\n                for line in child.strip().split(\"\\n\"):\n                    output.append(f\"{self.indent()}# {line}\")\n            elif isinstance(child, str) and child.strip():  # Text node\n                output.append(f\"{self.indent()}t({repr(child.strip())})\")\n        return output\n\n    def generate_app_root(self, node, generate_full_file=True):\n        header = (\n            [\n                \"from puepy import Application, Page, t\",\n                \"\",\n                \"app = Application()\",\n                \"\",\n                \"@app.page()\",\n                \"class DefaultPage(Page):\",\n                \"    def populate(self):\",\n            ]\n            if generate_full_file\n            else []\n        )\n        self.indent_level = 2 if generate_full_file else 0\n        body = self.iterate_node(node)\n        return \"\\n\".join(header + body)\n\n\ndef convert_html_to_context_manager(html, indent=4, generate_full_file=True):\n    soup = BeautifulSoup(html, \"html.parser\")\n    generator = TagGenerator(indentation=indent)\n    return generator.generate_app_root(soup, generate_full_file=generate_full_file)\n\n\n@app.page()\nclass DefaultPage(Page):\n    def initial(self):\n        return {\"input\": \"\", \"output\": \"\", \"error\": \"\", \"generate_full_file\": True}\n\n    def populate(self):\n        with t.div(classes=\"section\"):\n            t.h1(\"Convert HTML to PuePy syntax with BeautifulSoup\", classes=\"title is-1\")\n            with t.div(classes=\"columns is-variable is-8 is-multiline\"):\n                with t.div(classes=\"column is-half-desktop is-full-mobile\"):\n                    with t.div(classes=\"field\"):\n                        t.div(\"Enter HTML Here\", classes=\"label\")\n                        t.textarea(bind=\"input\", classes=\"textarea\")\n                with t.div(classes=\"column is-half-desktop is-full-mobile\"):\n                    with t.div(classes=\"field\"):\n                        t.div(\"Output\", classes=\"label\")\n                        t.textarea(bind=\"output\", classes=\"textarea\", readonly=True)\n            with t.div(classes=\"field is-grouped\"):\n                with t.p(classes=\"control\"):\n                    t.button(\"Convert\", classes=\"button is-primary\", on_click=self.on_convert_click)\n                with t.p(classes=\"control\"):\n                    with t.label(classes=\"checkbox\"):\n                        t.input(bind=\"generate_full_file\", type=\"checkbox\")\n                        t(\" Generate full file\")\n            if self.state[\"error\"]:\n                with t.div(classes=\"notification is-danger\"):\n                    t(self.state[\"error\"])\n\n    def on_convert_click(self, event):\n        self.state[\"error\"] = \"\"\n        try:\n            self.state[\"output\"] = convert_html_to_context_manager(\n                self.state[\"input\"], generate_full_file=self.state[\"generate_full_file\"]\n            )\n        except Exception as e:\n            self.state[\"error\"] = str(e)\n\n\napp.mount(\"#app\")\n

    PyScript documentation on packages

    For more information, including packages available to MicroPython, refer to the PyScript docs.

    "},{"location":"tutorial/09-using-web-components/","title":"Using Web Components","text":"

    Web Components are a collection of technologies, supported by all modern browsers, that let developers reuse custom components in a framework-agnostic way. Although PuePy is an esoteric framework, and no major component libraries exist for it (as they do with React or Vue), you can use Web Component widgets easily in PuePy and make use of common components available on the Internet.

    "},{"location":"tutorial/09-using-web-components/#using-shoelace","title":"Using Shoelace","text":"

    Shoelace is a popular and professionally developed suite of web components for building high quality user experiences. In this example, we'll see how to use Shoelace Web Components inside a project of ours. Here is a working example:

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/09_webcomponents/index.htmlEdit

    "},{"location":"tutorial/09-using-web-components/#adding-remote-assets","title":"Adding remote assets","text":"

    First, we'll need to load Shoelace from its CDN in our HTML file:

    index.html
    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/light.css\"/>\n<script type=\"module\"\n      src=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace-autoloader.js\"></script>\n
    "},{"location":"tutorial/09-using-web-components/#using-web-components-in-python","title":"Using Web Components in Python","text":"

    Because WebComponents are initialized just like other HTML tags, they can be used directly in Python:

    @app.page()\nclass DefaultPage(Page):\n    def populate(self):\n        with t.sl_dialog(label=\"Dialog\", classes=\"dialog-overview\", tag=\"sl-dialog\", ref=\"dialog\"):  # (1)!\n            t(\"Web Components are just delightful.\")\n            t.sl_button(\"Close\", slot=\"footer\", variant=\"primary\", on_click=self.on_close_click)  # (2)!\n        t.sl_button(\"Open Dialog\", tag=\"sl-button\", on_click=self.on_open_click)\n\n    def on_open_click(self, event):\n        self.refs[\"dialog\"].element.show()\n\n    def on_close_click(self, event):\n        self.refs[\"dialog\"].element.hide()\n
    1. The sl_dialog tag is a custom tag that creates a sl-dialog Web Component. It was defined by the Shoelace library we loaded via CDN. 2. The sl_button tag is another custom tag that creates a sl-button Web Component.

    "},{"location":"tutorial/09-using-web-components/#access-methods-and-properties-of-web-components","title":"Access methods and properties of web components","text":"

    Web Components are meant to be access directly, like this in JavaScript:

    <sl-dialog id=\"foo\"></sl-dialog>\n\n<script>\n    document.querySelector(\"#foo\").show()\n</script>\n

    The actual DOM elements are accessible in PuePy, but require using the .element attribute of the higher level Python instance of your tag:

    self.refs[\"dialog\"].element.show()\n
    "},{"location":"tutorial/10-full-app/","title":"A Full App Template","text":"

    Let's put together what we've learned so far. This example is an app with routing, a sidebar, and a handful of pages.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/10_full_app/index.htmlEdit

    URL Changes

    In the embedded example above, the \"URL\" does not change because the embedded example is not a full web page. In a full web page, the URL would change to reflect the current page. Try opening the example in a new window to see the URL change.

    "},{"location":"tutorial/10-full-app/#project-layout","title":"Project layout","text":"

    The larger example separates logic out into several files.

    • main.py: The Python file started from our <script> tag
    • common.py: A place to put objects common to other files
    • components.py: A place to put reusable components
    • pages.py: A place to put individual pages we navigate to
    "},{"location":"tutorial/10-full-app/#configuring-pyscript-for-multiple-files","title":"Configuring PyScript for multiple files","text":"

    To make additional source files available in the Python runtime environment, add them to the files list in the PyScript configuration file:

    pyscript-app.json
    {\n  \"name\": \"PuePy Tutorial\",\n  \"debug\": true,\n  \"files\": {\n    \"./common.py\": \"common.py\",\n    \"./components.py\": \"components.py\",\n    \"./main.py\": \"main.py\",\n    \"./pages.py\": \"pages.py\"\n  },\n  \"js_modules\": {\n    \"main\": {\n      \"https://cdn.jsdelivr.net/npm/chart.js\": \"chart\",\n      \"https://cdn.jsdelivr.net/npm/morphdom@2.7.2/+esm\": \"morphdom\"\n    }\n  },\n  \"packages\": [\n    \"../../puepy-0.6.2-py3-none-any.whl\"\n  ]\n}\n
    "},{"location":"tutorial/10-full-app/#adding-chartjs","title":"Adding Chart.js","text":"

    We also added a JavaScript library, chart.js, to the project.

      \"https://cdn.jsdelivr.net/npm/chart.js\": \"chart\"\n

    JavaScript Modules

    See JavaScript Modules in PyScript's documentation for additional information on loading JavaScript libraries into your project.

    We use charts.js directly from Python, in components.py:

    @t.component()\nclass Chart(Component):\n    props = [\"type\", \"data\", \"options\"]\n    enclosing_tag = \"canvas\"\n\n    def on_redraw(self):\n        self.call_chartjs()\n\n    def on_ready(self):\n        self.call_chartjs()\n\n    def call_chartjs(self):\n        if hasattr(self, \"_chart_js\"):\n            self._chart_js.destroy()\n\n        self._chart_js = js.Chart.new(\n            self.element,\n            jsobj(type=self.type, data=self.data, options=self.options),\n        )\n

    We call the JavaScript library in two places. When the component is added to the DOM (on_ready) and when it's going to be redrawn (on_redraw).

    "},{"location":"tutorial/10-full-app/#reusing-code","title":"Reusing Code","text":""},{"location":"tutorial/10-full-app/#a-common-app-layout","title":"A common app layout","text":"

    In components.py, we define a common application layout, then reuse it in multiple pages:

    class SidebarItem:\n    def __init__(self, label, icon, route):\n        self.label = label\n        self.icon = icon\n        self.route = route\n\n\n@t.component()\nclass AppLayout(Component):\n    sidebar_items = [\n        SidebarItem(\"Dashboard\", \"emoji-sunglasses\", \"dashboard_page\"),\n        SidebarItem(\"Charts\", \"graph-up\", \"charts_page\"),\n        SidebarItem(\"Forms\", \"input-cursor-text\", \"forms_page\"),\n    ]\n\n    def precheck(self):\n        if not self.application.state[\"authenticated_user\"]:\n            raise exceptions.Unauthorized()\n\n    def populate(self):\n        with t.sl_drawer(label=\"Menu\", placement=\"start\", classes=\"drawer-placement-start\", ref=\"drawer\"):\n            self.populate_sidebar()\n\n        with t.div(classes=\"container\"):\n            with t.div(classes=\"header\"):\n                with t.div():\n                    with t.sl_button(classes=\"menu-btn\", on_click=self.show_drawer):\n                        t.sl_icon(name=\"list\")\n                t.div(\"The Dapper App\")\n                self.populate_topright()\n            with t.div(classes=\"sidebar\", id=\"sidebar\"):\n                self.populate_sidebar()\n            with t.div(classes=\"main\"):\n                self.insert_slot()\n            with t.div(classes=\"footer\"):\n                t(\"Business Time!\")\n\n    def populate_topright(self):\n        with t.div(classes=\"dropdown-hoist\"):\n            with t.sl_dropdown(hoist=\"\"):\n                t.sl_icon_button(slot=\"trigger\", label=\"User Settings\", name=\"person-gear\")\n                with t.sl_menu(on_sl_select=self.on_menu_select):\n                    t.sl_menu_item(\n                        \"Profile\",\n                        t.sl_icon(slot=\"suffix\", name=\"person-badge\"),\n                        value=\"profile\",\n                    )\n                    t.sl_menu_item(\"Settings\", t.sl_icon(slot=\"suffix\", name=\"gear\"), value=\"settings\")\n                    t.sl_divider()\n                    t.sl_menu_item(\"Logout\", t.sl_icon(slot=\"suffix\", name=\"box-arrow-right\"), value=\"logout\")\n\n    def on_menu_select(self, event):\n        if event.detail.item.value == \"logout\":\n            self.application.state[\"authenticated_user\"] = \"\"\n\n    def populate_sidebar(self):\n        for item in self.sidebar_items:\n            with t.div():\n                with t.sl_button(\n                    item.label,\n                    variant=\"text\",\n                    classes=\"sidebar-button\",\n                    href=self.page.router.reverse(item.route),\n                ):\n                    if item.icon:\n                        t.sl_icon(name=item.icon, slot=\"prefix\")\n\n    def show_drawer(self, event):\n        self.refs[\"drawer\"].element.show()\n
    "},{"location":"tutorial/10-full-app/#loading-indicator","title":"Loading indicator","text":"

    Since CPython takes a while to load on slower connections, we'll populate the <div id=\"app> element with a loading indicator, which will be replaced once the application mounts:

    <div id=\"app\">\n  <div style=\"text-align: center; height: 100vh; display: flex; justify-content: center; align-items: center;\">\n    <sl-spinner style=\"font-size: 50px; --track-width: 10px;\"></sl-spinner>\n  </div>\n</div>\n
    "},{"location":"tutorial/10-full-app/#further-experimentation","title":"Further experimentation","text":"

    Don't forget to try cloning and modifying all the examples from this tutorial on PyScript.com.

    "}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"PuePy: Overview","text":"

    PuePy is a frontend web framework that builds on Python and Webassembly using PyScript. PuePy is truly a Python-first development environment. There is no transpiling to JavaScript; no Yarn, no NPM, no webpack, no Vite or Parcel. Python runs directly in your browser. PuePy is inspired by Vue.js, but is built entirely from scratch in Python.

    "},{"location":"#features","title":"Features","text":"
    • Reactivity: As components' state changes, redraws happen automatically
    • Component-Based Design: Encapsulate data, logic, and presentation in reusable components
    • Single-Class Components: Based vaguely on Vue's \"single file components\", each component and each page is a class.
    • Events, Slots, and Props: Events, slots, and props are all inspired by Vue and work similarly in PuePy
    • Minimal, Python: PuePy is built to use Pythonic conventions whenever possible, and eschews verbosity in the name of opinion.
    "},{"location":"#external-links","title":"External Links","text":"

    PuePy.dev Main Site GitHub

    "},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#philosophical-questions","title":"Philosophical Questions","text":""},{"location":"faq/#why-not-just-use-javascript","title":"Why not just use Javascript?","text":"

    If you prefer JavaScript to Python, using it would be the obvious answer. JavaScript has mature tooling available, there are many excellent frameworks to choose from, and the JavaScript runtimes available in most browsers are blazingly fast.

    Some developers prefer Python, however. For them, PuePy might be a good choice.

    "},{"location":"faq/#is-webassembly-ready","title":"Is WebAssembly ready?","text":"

    WebAssembly is supported in all major modern browsers, including Safari. Its standard has coalesced and for using existing JavaScript libraries or components, PyScript provides a robust bridge. WebAssembly is as ready as it needs to be and is certainly less prone to backwards incompatible changes than many JavaScript projects that production sites rely on.

    "},{"location":"faq/#puepy-design-choices","title":"PuePy Design Choices","text":""},{"location":"faq/#can-you-use-puepy-with-a-templating-language-instead-of-building-components-inline","title":"Can you use PuePy with a templating language instead of building components inline?","text":"

    The idea behind PuePy is, at least in part, to have the convenience of building all your software, including its UI, out in Python's syntax. You may actually find that Python is more succinct, not less, than a similar template might be. Consider:

    <h1>{{ name }}'s grocery shopping list</h1>\n<ul>\n\n</ul>\n<button on_click=\"buy()\">Buy Items</button>\n

    vs:

    with t.h1():\n    t(f\"{name}'s grocery shopping list\")\nwith t.ul():\n    for item in items:\n       t.li(item)\nt.button(\"Buy Items\", on_click=self.buy)\n

    If you have a whole HTML file ready to go, try out the HTML to Python converter built in the PypII libraries tutorial chapter, which uses BeautifulSoup.

    "},{"location":"faq/#can-i-use-svgs","title":"Can I use SVGs?","text":"

    Yes, as long as you specify xmlns:

    with t.svg(xmlns=\"http://www.w3.org/2000/svg\"):\n    ...\n
    "},{"location":"faq/#how-can-i-use-html-directly","title":"How can I use HTML directly?","text":"

    If you want to directly insert HTML into a component's rendering, you can use the html() string:

    from puepy.core import html\n\n\nclass MyPage(Page):\n    def populate(self):\n        t(html(\"<strong>Hello!</strong>\"))\n
    "},{"location":"installation/","title":"Installation","text":""},{"location":"installation/#client-side-installation","title":"Client-side installation","text":"

    Although PuePy is available on pypi, because PuePy is intended primarily as a client-side framework, \"installation\" is best achieved by downloading the wheel file and including it in your pyscript packages configuration.

    A simple first project (with no web server) would be:

    • index.html (index.html file)
    • pyscript.json (pyscript config file)
    • hello.py (Hello World code)
    • puepy-0.6.2-py3-none-any.whl (PuePy wheel file)

    The runtime file would contain only the files needed to actually execute PuePy code; no tests or other files. Runtime zips are available in each release's notes on GitHub.

    "},{"location":"installation/#downloading-client-runtime","title":"Downloading client runtime","text":"
    curl -O https://download.puepy.dev/puepy-0.6.2-py3-none-any.whl\n
    "},{"location":"installation/#setting-up-your-first-project","title":"Setting up your first project","text":"

    Continue to the tutorial to see how to set up your first project.

    "},{"location":"cookbook/loading-indicators/","title":"Showing Loading Indicators","text":"

    PyScript, on which PuePy is built, provides two runtime options. When combined with PuePy, the total transfer size to render a PuePy page as reported by Chromium's dev tools for each runtime are:

    Runtime Transfer Size MicroPython 353 KB Pyodide 5.9 MB

    MicroPython's runtime, even on a slower connection, is well within the bounds of normal web frameworks. Pyodide, however, will be perceived as initially quite slow to load on slower connections. Pyodide may be workable for internal line-of-business software where users have fast connections or in cases where it's accepted that an application may take some time to initially load, but will be cached during further use.

    "},{"location":"cookbook/loading-indicators/#showing-an-indicator-before-puepy-loads","title":"Showing an indicator before PuePy loads","text":"

    Before you mount your PuePy page into its target element, the target element's HTML is rendered in the browser. A very simple way to show that PuePy hasn't loaded is to include an indicator in the target element, which will be replaced upon execution by PuePy:

    <div id=\"app\">Loading...</div>\n

    The Full App Template example from the tutorial makes use of a Shoelace web component to show a visual loading indicator as a spinning wheel:

    <!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Example</title>\n    <link rel=\"stylesheet\" href=\"app.css\">\n\n    <link rel=\"stylesheet\" href=\"https://pyscript.net/releases/2025.2.2/core.css\">\n    <script type=\"module\" src=\"https://pyscript.net/releases/2025.2.2/core.js\"></script>\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/light.css\"/>\n    <script type=\"module\" src=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace.js\"></script>\n</head>\n<body>\n<!-- Show the application with a loading indicator that will be replaced later -->\n<div id=\"app\">\n    <div style=\"text-align: center; height: 100vh; display: flex; justify-content: center; align-items: center;\">\n        <sl-spinner style=\"font-size: 50px; --track-width: 10px;\"></sl-spinner>\n    </div>\n</div>\n<script type=\"mpy\" src=\"./main.py\" config=\"./pyscript-app.json\"></script>\n</body>\n</html>\n

    This will render as a loading indicator, animated, visible only until PuePy mounts the real application code:

    "},{"location":"cookbook/navigation-guards/","title":"Navigation Guards","text":"

    When a page loads, you can guard navigation to that page by running a precheck \u2013 a method that runs before the page is rendered. If the precheck raises an exception, the page is not rendered, and the exception is caught by the framework. This is useful for checking if a user is authenticated, for example.

    Here's an example of a precheck that raises an exception if the user is not authenticated:

    Showing error
    from puepy import exceptions, Page\n\nclass MyPage(Page):\n    ...\n    def precheck(self):\n        if not self.application.state[\"authenticated_user\"]:\n            raise exceptions.Unauthorized()\n

    In this example, if the authenticated_user key in the application state is False, the page will not render, and an Unauthorized exception will be raised. PuePy will then display your application.unauthorized_page.

    Alternatively, you could redirect the user by raising puepu.exceptions.Redirect:

    Redirecting to a login page
    from puepy import exceptions, Page\n\n\nclass LoginPage(Page):\n    ...\n\n\nclass MyPage(Page):\n    ...\n    def precheck(self):\n        if not self.application.state[\"authenticated_user\"]:\n            raise exceptions.Redirect(LoginPage)\n
    "},{"location":"guide/advanced-routing/","title":"Router","text":"

    Client-side routing in PuePy is optional. If enabled, the router allows you to define multiple \"pages\" with their own URLs.

    See Also

    • Tutorial: Routing
    • Reference: puepy.router

    If you do not install the router, you can only define one page, and that page will be mounted on the target element. If you install the router, the browser's URL will determine which page is mounted, based on the link mode used.

    "},{"location":"guide/advanced-routing/#installing-the-router","title":"Installing the router","text":"

    Routing is an entirely optional feature of PuePy. Many projects may prefer to rely on backend-side routing, like a traditional web project. However if you are developing a single-page app, or simply want to use multiple \"subpages\" on the page you made using PuePy, you must install the router by calling app.install_router.

    from puepy import Application\nfrom puepy.router import Router\n\n\napp = Application()\napp.install_router(Router, link_mode=Router.LINK_MODE_HASH)\n

    link_mode defines how PuePy both creates and parses URLs. There are three options:

    link_mode description pro/con Router.LINK_MODE_HASH Uses window location \"anchor\" (the part of the URL after a #) to determine route Simple implementation, works with any backend Router.LINK_MODE_HTML5 Uses browser's history API to manipulate page URLs without causing a reload Cleaner URLs (but requires backend configuration) Router.LINK_MODE_DIRECT Keeps routing information on the client, but links directly to pages, causing reload Might be ideal for niche use cases or server-side rendering

    Tip

    If you want to use client-side routing but aren't sure what link_mode to enable, \"hash\" is probably your best bet.

    "},{"location":"guide/advanced-routing/#accessing-the-router-object","title":"Accessing the Router object","text":"

    Once enabled, the Router object may be accessed on any component using self.page.router.

    "},{"location":"guide/advanced-routing/#defining-routes","title":"Defining routes","text":"

    The preferred way of adding pages to the router is by calling the @app.page decorator on your Application object (see Hello World in the tutorial.

    from puepy import Page\n\n\n@app.page(\"/my-page\")\nclass MyPage(Page):\n    ...\n

    Or, define a default route by not passing a path.

    @app.page()\nclass MyPage(Page):\n    ...\n

    You can also add routes directly, though this isn't the preferred method.

    app.router.add_route(path_match=\"/foobar\", page_class=Foobar)\n
    "},{"location":"guide/advanced-routing/#route-names","title":"Route names","text":"

    By default, routes are named by converting the class name to lower case, and replacing MixedCase with under_scores. For instance, MyPage would be converted to my_page as a route name. You may, however, give your routes custom names by passing a name parameter to either add_route or @app.page:

    @app.page(\"/my-page\", name=\"another_name\")\nclass MyPage(Page):\n    ...\n\n\nclass AnotherPage(Page):\n    ...\n\n\napp.add_route(\"/foobar\", AnotherPage, name=\"foobar\")\n
    "},{"location":"guide/advanced-routing/#passing-parameters-to-pages","title":"Passing parameters to pages","text":"

    Pages can accept parameters fropm the router with props matching placeholders defined in the route path.

    @app.page(\"/post/<author_id>/<post_id>/view\")\nclass PostPage(Page):\n    props = [\"author_id\", \"post_id\"]\n\n    def populate(self):\n        t.p(f\"This is a post from {self.author_id}->{self.user_id}\")\n
    "},{"location":"guide/advanced-routing/#reversing-routes","title":"Reversing routes","text":"

    Call router.reverse with the page you want to find the route for, along with any relevant arguments.

    path = self.page.router.reverse(PostPage, author_id=\"5\", post_id=\"7\")\n

    The page can either be the Page object itself, or the route name.

    path = self.page.router.reverse(\"post_page\", author_id=\"5\", post_id=\"7\")\n
    "},{"location":"guide/css-classes/","title":"CSS Classes","text":"

    Although you are in charge of your own CSS, PuePy provides some convenience mechanisms for defining CSS classes. Because class is a reserved word in Python, when passing classes to tags, you should use either class_name or classes. Each can be defined as a string, list, or dictionary:

    @app.page()\nclass HelloWorldPage(Page):\n    def populate(self):\n        t.button(\"Primary Large Button\", class_name=\"primary large\")\n        t.button(\"Primary Small Button\", classes=[\"primary\", \"small\"])\n        t.button(\"Primary Medium Button\", classes={\n            \"primary\": True, \n            \"medium\": True, \n            \"small\": False, \n            \"large\": False})\n

    Notice that when passing a dictionary, the value of the dictionary indicates whether the class will be included.

    "},{"location":"guide/css-classes/#components-and-classes","title":"Components and classes","text":"

    Components can define default classes. For example in the Components section of the tutorial, we define a Card component:

    @t.component()\nclass Card(Component):\n    ...\n\n    default_classes = [\"card\"]\n\n    ...\n

    The default_classes attribute tells PuePy to render the component with card as a default class. Code using the Card component can add to or even remove the default classes defined by the component.

    To remove a class, pass it with a \"/\" prefix:

    class MyPage(Page):\n    def populate(self):\n        # This will render as a div with both \"card\" and \"card-blue\" \n        # classes.\n        t.card(classes=\"card-blue\")\n\n        # This will override the default and remove the \"card\" class\n        t.card(classes=\"/card\")        \n
    "},{"location":"guide/in-depth-components/","title":"In-Depth Components","text":"

    Defining components in PuePy is a powerful way to encapsulate data, display, and logic in a reusable way. Components become usable like tags in the populate() method of other components or pages, define slots, props, and events.

    "},{"location":"guide/in-depth-components/#data-flow-features","title":"Data flow features","text":""},{"location":"guide/in-depth-components/#slots","title":"Slots","text":"

    Slots are a mechanism to allow parent components to inject content (tags, other components, etc) into child components in specified locations. There can be one default or unamed slot, and any number of named slots.

    • Slots are defined in the populate() method of a component or page using self.insert_slot.
    • Slots are consumed in the code using the component with a context manager object and <component>.slot().

    See the Components Tutorial Chapter for more information on slots.

    "},{"location":"guide/in-depth-components/#props","title":"Props","text":"

    Props are a way to pass data to child components. Props must be defined by a component. When writing a component, you can simply include props as a list of strings, where each element is the name of a prop, or include instances of the Prop class. You can mix and match the two as well

    class MyComponent(Component):\n    props = [\n        \"title\",  # (1)!\n        Prop(\"author_name\", \"Name of Author\", str, \"Unknown\") # (2)!\n    ]\n
    1. This is a prop which only defines a name.
    2. To add extra metadata about a prop, you can also define a Prop instance.

    Regardless of how you define props in your component, a full \"expanded\" list of props is available on self.props_expanded as a dictionary mapping prop name to Prop instance, with the Prop instance created automatically if only a name is specified.

    See Also

    • Prop Class Reference
    "},{"location":"guide/in-depth-components/#attributes","title":"Attributes","text":"

    Keyword arguments passed to a component that do not match any known prop are considered attributes and stored in self.attrs. They are then inserted into the rendered DOM as HTML elements on the rendered attribute. This means that, for instance, you can pass arbitrary HTML attributes to newly created components without defining any custom props or other logic. Eg,

    from puepy import Component, Page, t\n\n\nclass NameInput(Component):\n    enclosing_tag = \"input\"\n\nclass MyPage(Page):\n    def populate(self):\n        t.name_input(id=\"name_input\", placeholder=\"Enter your name\")\n

    Even if the NameInput component does not define a placeholder prop, the placeholder attribute will be rendered on the input tag.

    When to use props vs attributes?

    • Use props when you want to pass data to a component that will be used in the component's logic or rendering.
    • Use attributes when you want to pass data to a component that will be used in the rendered HTML but not in the component's logic.
    "},{"location":"guide/in-depth-components/#events","title":"Events","text":"

    Events are a way to allow child components to communicate with parent components. When writing a component, in your own code, you can emit an event by calling self.trigger_event. You can also optionally pass a detail dictionary to the event, which will be passed along (after Python to JavaScript conversion) to the browser's native event system in JavaScript.

    For example, suppose you want to emit a custom event, greeting, with a type attribute:

    class MyComponent(Component):\n    def some_method(self):\n        self.trigger_event(\"greeting\", detail={\"message\": \"Hello There\"})\n

    A consumer of your component can listen for this event by defining an on_greeting method in their component or page:

    class MyPage(Page):\n    def populate(self):\n        t.my_component(on_greeting=self.on_greeting_sent)\n\n    def on_greeting_sent(self, event):\n        print(\"Incoming message from component\", event.detail.get('message'))\n

    See Also

    Mozilla's guide to JavaScript events

    "},{"location":"guide/in-depth-components/#customization","title":"Customization","text":"

    You have several ways of controlling how your components are rendered. First, you can define what enclosing tag your component is rendered as. The default is a div tag, but this can be overridden:

    class MyInputComponent(Component):\n    enclosing_tag = \"input\"\n

    You can also define default classes, default attributes, and the default role for your component:

    class MyInputComponent(Component):\n    enclosing_tag = \"input\"\n\n    default_classes = [\"my-input\"]\n    default_attributes = {\"type\": \"text\"}\n    default_role = \"textbox\"\n
    "},{"location":"guide/in-depth-components/#parentchild-relationships","title":"Parent/Child relationships","text":"

    Each tag (and thus each component) in PuePy has a parent unless it is the root page. Consider the following example:

    from puepy import Application, Page, Component, t\n\napp = Application()\n\n\n@t.component()\nclass CustomInput(Component):\n    enclosing_tag = \"input\"\n\n    def on_event_handle(self, event):\n        print(self.parent)\n\n\nclass MyPage(Page):\n    def populate(self):\n        with t.div():\n            t.custom_input()\n

    In this example, the parent of the CustomInput instance is not the MyPage instance, it is the div, a puepy.Tag instance. In many cases, you will want to interact another relevant object, not necessarily the one immediately parental of your current instance. In those instances, from your components, you may reference:

    • self.page (Page instance): The page ultimately responsible for rendering this component
    • self.origin (Component or Page instance): The component that created yours in its populate() method
    • self.parent (Tag instance): The direct parent of the current instance

    Additionally, parent instances have the following available:

    • self.children (list): Direct child nodes
    • self.refs (dict): Instances created during this instance's most recent populate() method

    Warning

    None of the attributes regarding parent/child/origin relationships should be modified by application code. Doing so could result in unexpected behavior.

    "},{"location":"guide/in-depth-components/#refs","title":"Refs","text":"

    In addition to parent/child relationships, most components and pages define an entire hierarchy of tags and components in the populate() method. If you want to reference components later, or tell PuePy which component is which (in case the ordering changes in sebsequent redraws), using a ref= argument when building tags:

    class MyPage(Page):\n    def populate(self):\n        t.button(\"My Button\", ref=\"my_button\")\n\n    def auto_click_button(self, ...):\n        self.refs[\"my_button\"].element.click()\n

    For more information on why this is useful, see the Refs Tutorial Topic.

    "},{"location":"guide/pyscript-config/","title":"PyScript Config","text":"

    PyScript's configuration is fully documented in the PyScript documentation. Configuration for PuePy simply requires adding the PuePy runtime files (see Quick Start - Installation) and Morphdom:

    {\n  \"name\": \"PuePy Tutorial\",\n  \"debug\": true,\n  \"packages\": [\n    \"./puepy-0.6.2-py3-none-any.whl\"\n  ],\n  \"js_modules\": {\n    \"main\": {\n      \"https://cdn.jsdelivr.net/npm/morphdom@2.7.4/+esm\": \"morphdom\"\n    }\n  }\n}\n
    "},{"location":"guide/reactivity/","title":"Reactivity","text":"

    Reactivity is a paradigm that causes the user interface to update automatically in response to changes in the application state. Rather than triggering updates manually as a programmer, you can be assured that the application state will trigger redraws, with new information, as needed. Reactivity in PuePy is inspired by Vue.js.

    "},{"location":"guide/reactivity/#state","title":"State","text":""},{"location":"guide/reactivity/#initial-state","title":"Initial state","text":"

    Components (including Pages) define initial state through the initial() method:

    class MyComponent(Component):\n    def initial(self):\n        return {\n            \"name\": \"Monty ... Something?\",\n            \"movies\": [\"Monty Python and the Holy Grail\"]\n        }\n
    "},{"location":"guide/reactivity/#modifying-state","title":"Modifying state","text":"

    If any method on the component changed the name, it would trigger a UI refresh:

    class MyComponent(Component):\n    def update_name(self):\n        # This triggers a refresh\n        self.state[\"name\"] = \"Monty Python\"\n
    "},{"location":"guide/reactivity/#modifying-mutable-objects-in-place","title":"Modifying mutable objects in-place","text":"

    Warnign

    PuePy's reactivity works by using dictionary __setitem__ and __delitem__ methods. As such, it cannot detect \"nested\" updates or changes to mutable objects in the state. If your code will result in a state change such as a data structure being changed in-place, you must a mutate() context manager.

    Modifying complex (mutable) data structures in place without setting them will not work:

    class MyComponent(Component):\n    def update_movies(self):\n        # THIS WILL NOT CAUSE A UI REFRESH!\n        self.state[\"movies\"].append(\"Monty Python\u2019s Life of Brian\")\n

    Instead, use a context manager to tell the state object what is being modified. This is ideal anyway.

    class MyComponent(Component):\n    def update_movies(self):\n        # THIS WILL NOT CAUSE A UI REFRESH!\n        with self.state.mutate(\"movies\"):\n            self.state[\"movies\"].append(\"Monty Python\u2019s Life of Brian\")\n

    mutate(*keys) can be called with any number of keys you intend to modify. As an added benefit, the state change will only call listeners after the context manager exits, making it ideal also for \"batching up\" changes.

    "},{"location":"guide/reactivity/#controlling-ui-refresh","title":"Controlling UI Refresh","text":""},{"location":"guide/reactivity/#disabling-automatic-refresh","title":"Disabling Automatic Refresh","text":"

    By default, any detected mutations to a component's state will trigger a UI fresh. This can be customized. To disable automatic refresh entirely, set redraw_on_changes to False.

    class MyComponent(Component):\n    # The UI will no longer refresh on state changes\n    redraw_on_changes = False\n\n    def something_happened(self):\n        # This can be called to manually refresh this component and its children\n        self.trigger_redraw()\n\n        # Or, you can redraw the whole page\n        self.page.trigger_redraw()\n
    "},{"location":"guide/reactivity/#limiting-automatic-refresh","title":"Limiting Automatic Refresh","text":"

    Suppose that you want to refresh the UI on some state changes, but not others.

    class MyComponent(Component):\n    # When items in this this change, the UI will be redrawn\n    redraw_on_changes = [\"items\"]\n
    "},{"location":"guide/reactivity/#watching-for-changes","title":"Watching for changes","text":"

    You can watch for changes in state yourself.

    class MyComponent(Component):\n    def initial():\n        return {\"spam\": \"eggs\"}\n\n    def on_spam_change(self, new_value):\n        print(\"New value for spam\", new_value)\n

    Or, watch for any state change:

    class MyComponent(Component):\n    def on_state_change(self, key, value):\n        print(key, \"was set to\", value)\n
    "},{"location":"guide/reactivity/#binding-form-element-values-to-state","title":"Binding form element values to state","text":"

    For your convenience, the bind parameter can be used to automatically establish a two-way connection between input elements and component state. When the value of a form element changes, the state is updated. When the state is updated, the corresponding form tag's value reflects that change.

    class MyComponent(Component):\n    def initial(self):\n        return {\"name\": \"\"}\n\n    def populate(self):\n        # bind specifies what key on self.state should be tied to this input's value\n        t.input(placeholder=\"Type your name\", bind=\"name\")\n
    "},{"location":"guide/reactivity/#application-state","title":"Application State","text":"

    In addition to components and pages, there is also a \"global\" application-wide state. Note that this state is only for a running Application instance and does not survive reloads nor is it shared across multiple browser tabs or windows.

    To use the application state, use application.state as you would local state. For example, in the Full App Template tutorial chapter, the working example uses self.application.state[\"authenticated_user\"] in a variety of places:

    Navigation Guard
    def precheck(self):\n    if not self.application.state[\"authenticated_user\"]:\n        raise exceptions.Unauthorized()\n
    Setting state
    self.application.state[\"authenticated_user\"] = self.state[\"username\"]\n
    Rendering based on application state
    def populate(self):\n    ...\n\n    t.h1(f\"Hello, you are authenticated as {self.application.state['authenticated_user']}\")\n

    As with page or component state, changes to the application state trigger refreshes by default. That behavior can be controlled with redraw_on_app_state_changes on components or pages:

    class Page1(Page):\n    redraw_on_app_state_changes = True  # (1)!\n\n\nclass Page2(Page):\n    redraw_on_app_state_changes = False  # (2)!\n\n\nclass Page3(Page):\n    redraw_on_app_state_changes = [\"authenticated_user\"]  # (3)!\n
    1. The default behavior, with redraw_on_app_state_changes set to True, all changes to application state trigger a redraw.
    2. Setting redraw_on_app_state_changes to False prevents changes to application state from triggering a redraw.
    3. Setting redraw_on_app_state_changes to a list of keys will trigger a redraw only when those keys change.

    Tip

    This behavior mirrors redraw_on_state_changes, which is used for local state.

    "},{"location":"guide/runtimes/","title":"Runtimes","text":"

    From its upstream project, PyScript, PuePy supports two runtime environments:

    • MicroPython
    • Pyodide

    There are some interface differences, as well as technical ones, described in the official PyScript docs. Additionally, many standard library features are missing from MicroPython. MicroPython does not have access to PyPi packages, nor does MicroPython type hinting or other advanced features as thoroughly as Pyodide.

    MicroPython, however, has just a ~170k runtime, making it small enough to load on \"normal\" websites without the performance hit of Pyodide's 11MB runtime. It is ideal for situations where webpage response time is important.

    "},{"location":"guide/runtimes/#when-to-use-pyodide","title":"When to use Pyodide","text":"

    You may consider using Pyodide when:

    • Initial load time is less important
    • You need to use PyPi packages
    • You need to use advanced Python features
    • You need to use the full Python standard library
    • You need to use type hinting
    • You need to use Python 3.9 or later
    "},{"location":"guide/runtimes/#when-to-use-micropython","title":"When to use MicroPython","text":"

    You may consider using MicroPython when:

    • Initial load time is important
    • Your PuePy code will use only simple Python features to add reactivity and interactivity to websites
    "},{"location":"guide/runtimes/#how-to-switch-runtimes","title":"How to switch runtimes","text":"

    To choose a runtime, specify either type=\"mpy\" or type=\"py\" in your <script> tag when loading PuePy. For example:

    "},{"location":"guide/runtimes/#loading-pyodide","title":"Loading Pyodide","text":"
    <script type=\"mpy\" src=\"./main.py\" config=\"pyscript.json\"></script>\n
    "},{"location":"guide/runtimes/#loading-micropython","title":"Loading MicroPython","text":"
    <script type=\"mpy\" src=\"./main.py\" config=\"pyscript.json\"></script>\n

    See Also

    • PyScript Architecture: Interpreters
    • Pyodide Project
    • MicroPython Project
    "},{"location":"reference/application/","title":"puepy.Application","text":"

    The puepy.Application class is a core part of PuePy. It is the main entry point for creating PuePy applications. The Application class is used to manage the application's state, components, and pages.

    Bases: Stateful

    The main application class for PuePy. It manages the state, storage, router, and pages for the application.

    Attributes:

    Name Type Description state ReactiveDict

    The state object for the application.

    session_storage BrowserStorage

    The session storage object for the application.

    local_storage BrowserStorage

    The local storage object for the application.

    router Router

    The router object for the application, if any

    default_page Page

    The default page to mount if no route is matched.

    active_page Page

    The currently active page.

    not_found_page Page

    The page to mount when a 404 error occurs.

    forbidden_page Page

    The page to mount when a 403 error occurs.

    unauthorized_page Page

    The page to mount when a 401 error occurs.

    error_page Page

    The page to mount when an error occurs.

    Source code in puepy/application.py
    class Application(Stateful):\n    \"\"\"\n    The main application class for PuePy. It manages the state, storage, router, and pages for the application.\n\n    Attributes:\n        state (ReactiveDict): The state object for the application.\n        session_storage (BrowserStorage): The session storage object for the application.\n        local_storage (BrowserStorage): The local storage object for the application.\n        router (Router): The router object for the application, if any\n        default_page (Page): The default page to mount if no route is matched.\n        active_page (Page): The currently active page.\n        not_found_page (Page): The page to mount when a 404 error occurs.\n        forbidden_page (Page): The page to mount when a 403 error occurs.\n        unauthorized_page (Page): The page to mount when a 401 error occurs.\n        error_page (Page): The page to mount when an error occurs.\n    \"\"\"\n\n    def __init__(self, element_id_generator=None):\n        self.state = ReactiveDict(self.initial())\n        self.add_context(\"state\", self.state)\n\n        if is_server_side:\n            self.session_storage = None\n            self.local_storage = None\n        else:\n            from js import localStorage, sessionStorage\n\n            self.session_storage = BrowserStorage(sessionStorage, \"session_storage\")\n            self.local_storage = BrowserStorage(localStorage, \"local_storage\")\n        self.router = None\n        self._selector_or_element = None\n        self.default_page = None\n        self.active_page = None\n\n        self.not_found_page = GenericErrorPage\n        self.forbidden_page = GenericErrorPage\n        self.unauthorized_page = GenericErrorPage\n        self.error_page = TracebackErrorPage\n\n        self.element_id_generator = element_id_generator or DefaultIdGenerator()\n\n    def install_router(self, router_class, **kwargs):\n        \"\"\"\n        Install a router in the application.\n\n        Args:\n            router_class (class): A class that implements the router logic for the application. At this time, only\n                `puepy.router.Router` is available.\n            **kwargs: Additional keyword arguments that can be passed to the router_class constructor.\n        \"\"\"\n        self.router = router_class(application=self, **kwargs)\n        if not is_server_side:\n            add_event_listener(window, \"popstate\", self._on_popstate)\n\n    def page(self, route=None, name=None):\n        \"\"\"\n        A decorator for `Page` classes which adds the page to the application with a specified route and name.\n\n        Intended to be called as a decorator.\n\n        Args:\n            route (str): The route for the page. Default is None.\n            name (str): The name of the page. If left None, page class is used as the name.\n\n        Examples:\n            ``` py\n            app = Application()\n            @app.page(\"/my-page\")\n            class MyPage(Page):\n                ...\n            ```\n        \"\"\"\n        if route:\n            if not self.router:\n                raise Exception(\"Router not installed\")\n\n            def decorator(func):\n                self.router.add_route(route, func, name=name)\n                return func\n\n            return decorator\n        else:\n\n            def decorator(func):\n                self.default_page = func\n                return func\n\n            return decorator\n\n    def _on_popstate(self, event):\n        if self.router.link_mode == self.router.LINK_MODE_HASH:\n            self.mount(self._selector_or_element, window.location.hash.split(\"#\", 1)[-1])\n        elif self.router.link_mode in (self.router.LINK_MODE_DIRECT, self.router.LINK_MODE_HTML5):\n            self.mount(self._selector_or_element, window.location.pathname)\n\n    def remount(self, path=None, page_kwargs=None):\n        \"\"\"\n        Remounts the selected element or selector with the specified path and page_kwargs.\n\n        Args:\n            path (str): The new path to be used for remounting the element or selector. Default is None.\n            page_kwargs (dict): Additional page kwargs to be passed when remounting. Default is None.\n\n        \"\"\"\n        self.mount(self._selector_or_element, path=path, page_kwargs=page_kwargs)\n\n    def mount(self, selector_or_element, path=None, page_kwargs=None):\n        \"\"\"\n        Mounts a page onto the specified selector or element with optional path and page_kwargs.\n\n        Args:\n            selector_or_element: The selector or element on which to mount the page.\n            path: Optional path to match against the router. Defaults to None.\n            page_kwargs: Optional keyword arguments to pass to the mounted page. Defaults to None.\n\n        Returns:\n            (Page): The mounted page instance\n        \"\"\"\n        if page_kwargs is None:\n            page_kwargs = {}\n\n        self._selector_or_element = selector_or_element\n\n        if self.router:\n            path = path or self.current_path\n            route, arguments = self.router.match(path)\n            if arguments:\n                page_kwargs.update(arguments)\n\n            if route:\n                page_class = route.page\n            elif path in (\"\", \"/\") and self.default_page:\n                page_class = self.default_page\n            elif self.not_found_page:\n                page_class = self.not_found_page\n            else:\n                return None\n        elif self.default_page:\n            route = None\n            page_class = self.default_page\n        else:\n            return None\n\n        self.active_page = None\n        try:\n            self.mount_page(\n                selector_or_element=selector_or_element,\n                page_class=page_class,\n                route=route,\n                page_kwargs=page_kwargs,\n                handle_exceptions=True,\n            )\n        except Exception as e:\n            self.handle_error(e)\n        return self.active_page\n\n    @property\n    def current_path(self):\n        \"\"\"\n        Returns the current path based on the router's link mode.\n\n        Returns:\n            str: The current path.\n        \"\"\"\n        if self.router.link_mode == self.router.LINK_MODE_HASH:\n            return window.location.hash.split(\"#\", 1)[-1]\n        elif self.router.link_mode in (self.router.LINK_MODE_DIRECT, self.router.LINK_MODE_HTML5):\n            return window.location.pathname\n        else:\n            return \"\"\n\n    def mount_page(self, selector_or_element, page_class, route, page_kwargs, handle_exceptions=True):\n        \"\"\"\n        Mounts a page on the specified selector or element with the given parameters.\n\n        Args:\n            selector_or_element (str or Element): The selector string or element to mount the page on.\n            page_class (class): The page class to mount.\n            route (str): The route for the page.\n            page_kwargs (dict): Additional keyword arguments to pass to the page class.\n            handle_exceptions (bool, optional): Determines whether to handle exceptions thrown during mounting.\n                Defaults to True.\n        \"\"\"\n        page_class._expanded_props()\n\n        # For security, we only pass props to the page that are defined in the page's props\n        #\n        # We also handle the list or not-list props for multiple or single values\n        # (eg, ?foo=1&foo=2 -> [\"1\", \"2\"] if needed)\n        #\n        prop_args = {}\n        prop: Prop\n        for prop in page_class.props_expanded.values():\n            if prop.name in page_kwargs:\n                value = page_kwargs.pop(prop.name)\n                if prop.type is list:\n                    prop_args[prop.name] = value if isinstance(value, list) else [value]\n                else:\n                    prop_args[prop.name] = value if not isinstance(value, list) else value[0]\n\n        self.active_page: Page = page_class(matched_route=route, application=self, extra_args=page_kwargs, **prop_args)\n        try:\n            self.active_page.mount(selector_or_element)\n        except exceptions.PageError as e:\n            if handle_exceptions:\n                self.handle_page_error(e)\n            else:\n                raise\n\n    def handle_page_error(self, exc):\n        \"\"\"\n        Handles page error based on the given exception by inspecting the exception type and passing it along to one\n        of:\n\n        - `handle_not_found`\n        - `handle_forbidden`\n        - `handle_unauthorized`\n        - `handle_redirect`\n        - `handle_error`\n\n        Args:\n            exc (Exception): The exception object representing the page error.\n        \"\"\"\n        if isinstance(exc, exceptions.NotFound):\n            self.handle_not_found(exc)\n        elif isinstance(exc, exceptions.Forbidden):\n            self.handle_forbidden(exc)\n        elif isinstance(exc, exceptions.Unauthorized):\n            self.handle_unauthorized(exc)\n        elif isinstance(exc, exceptions.Redirect):\n            self.handle_redirect(exc)\n        else:\n            self.handle_error(exc)\n\n    def handle_not_found(self, exception):\n        \"\"\"\n        Handles the exception for not found page. By default, it mounts the self.not_found_page class and passes it\n        the exception as an argument.\n\n        Args:\n            exception (Exception): The exception that occurred.\n        \"\"\"\n        self.mount_page(\n            self._selector_or_element, self.not_found_page, None, {\"error\": exception}, handle_exceptions=False\n        )\n\n    def handle_forbidden(self, exception):\n        \"\"\"\n        Handles the exception for forbidden page. By default, it mounts the self.forbidden_page class and passes it\n        the exception as an argument.\n\n        Args:\n            exception (Exception): The exception that occurred.\n        \"\"\"\n        self.mount_page(\n            self._selector_or_element,\n            self.forbidden_page,\n            None,\n            {\"error\": exception},\n            handle_exceptions=False,\n        )\n\n    def handle_unauthorized(self, exception):\n        \"\"\"\n        Handles the exception for unauthorized page. By default, it mounts the self.unauthorized_page class and passes it\n        the exception as an argument.\n\n        Args:\n            exception (Exception): The exception that occurred.\n        \"\"\"\n        self.mount_page(\n            self._selector_or_element, self.unauthorized_page, None, {\"error\": exception}, handle_exceptions=False\n        )\n\n    def handle_error(self, exception):\n        \"\"\"\n        Handles the exception for application or unknown errors. By default, it mounts the self.error_page class and\n        passes it the exception as an argument.\n\n        Args:\n            exception (Exception): The exception that occurred.\n        \"\"\"\n        self.mount_page(self._selector_or_element, self.error_page, None, {\"error\": exception}, handle_exceptions=False)\n        if is_server_side:\n            raise\n\n    def handle_redirect(self, exception):\n        \"\"\"\n        Handles a redirect exception by navigating to the given path.\n\n        Args:\n            exception (RedirectException): The redirect exception containing the path to navigate to.\n        \"\"\"\n        self.router.navigate_to_path(exception.path)\n
    "},{"location":"reference/application/#puepy.Application.current_path","title":"current_path property","text":"

    Returns the current path based on the router's link mode.

    Returns:

    Name Type Description str

    The current path.

    "},{"location":"reference/application/#puepy.Application.handle_error","title":"handle_error(exception)","text":"

    Handles the exception for application or unknown errors. By default, it mounts the self.error_page class and passes it the exception as an argument.

    Parameters:

    Name Type Description Default exception Exception

    The exception that occurred.

    required Source code in puepy/application.py
    def handle_error(self, exception):\n    \"\"\"\n    Handles the exception for application or unknown errors. By default, it mounts the self.error_page class and\n    passes it the exception as an argument.\n\n    Args:\n        exception (Exception): The exception that occurred.\n    \"\"\"\n    self.mount_page(self._selector_or_element, self.error_page, None, {\"error\": exception}, handle_exceptions=False)\n    if is_server_side:\n        raise\n
    "},{"location":"reference/application/#puepy.Application.handle_forbidden","title":"handle_forbidden(exception)","text":"

    Handles the exception for forbidden page. By default, it mounts the self.forbidden_page class and passes it the exception as an argument.

    Parameters:

    Name Type Description Default exception Exception

    The exception that occurred.

    required Source code in puepy/application.py
    def handle_forbidden(self, exception):\n    \"\"\"\n    Handles the exception for forbidden page. By default, it mounts the self.forbidden_page class and passes it\n    the exception as an argument.\n\n    Args:\n        exception (Exception): The exception that occurred.\n    \"\"\"\n    self.mount_page(\n        self._selector_or_element,\n        self.forbidden_page,\n        None,\n        {\"error\": exception},\n        handle_exceptions=False,\n    )\n
    "},{"location":"reference/application/#puepy.Application.handle_not_found","title":"handle_not_found(exception)","text":"

    Handles the exception for not found page. By default, it mounts the self.not_found_page class and passes it the exception as an argument.

    Parameters:

    Name Type Description Default exception Exception

    The exception that occurred.

    required Source code in puepy/application.py
    def handle_not_found(self, exception):\n    \"\"\"\n    Handles the exception for not found page. By default, it mounts the self.not_found_page class and passes it\n    the exception as an argument.\n\n    Args:\n        exception (Exception): The exception that occurred.\n    \"\"\"\n    self.mount_page(\n        self._selector_or_element, self.not_found_page, None, {\"error\": exception}, handle_exceptions=False\n    )\n
    "},{"location":"reference/application/#puepy.Application.handle_page_error","title":"handle_page_error(exc)","text":"

    Handles page error based on the given exception by inspecting the exception type and passing it along to one of:

    • handle_not_found
    • handle_forbidden
    • handle_unauthorized
    • handle_redirect
    • handle_error

    Parameters:

    Name Type Description Default exc Exception

    The exception object representing the page error.

    required Source code in puepy/application.py
    def handle_page_error(self, exc):\n    \"\"\"\n    Handles page error based on the given exception by inspecting the exception type and passing it along to one\n    of:\n\n    - `handle_not_found`\n    - `handle_forbidden`\n    - `handle_unauthorized`\n    - `handle_redirect`\n    - `handle_error`\n\n    Args:\n        exc (Exception): The exception object representing the page error.\n    \"\"\"\n    if isinstance(exc, exceptions.NotFound):\n        self.handle_not_found(exc)\n    elif isinstance(exc, exceptions.Forbidden):\n        self.handle_forbidden(exc)\n    elif isinstance(exc, exceptions.Unauthorized):\n        self.handle_unauthorized(exc)\n    elif isinstance(exc, exceptions.Redirect):\n        self.handle_redirect(exc)\n    else:\n        self.handle_error(exc)\n
    "},{"location":"reference/application/#puepy.Application.handle_redirect","title":"handle_redirect(exception)","text":"

    Handles a redirect exception by navigating to the given path.

    Parameters:

    Name Type Description Default exception RedirectException

    The redirect exception containing the path to navigate to.

    required Source code in puepy/application.py
    def handle_redirect(self, exception):\n    \"\"\"\n    Handles a redirect exception by navigating to the given path.\n\n    Args:\n        exception (RedirectException): The redirect exception containing the path to navigate to.\n    \"\"\"\n    self.router.navigate_to_path(exception.path)\n
    "},{"location":"reference/application/#puepy.Application.handle_unauthorized","title":"handle_unauthorized(exception)","text":"

    Handles the exception for unauthorized page. By default, it mounts the self.unauthorized_page class and passes it the exception as an argument.

    Parameters:

    Name Type Description Default exception Exception

    The exception that occurred.

    required Source code in puepy/application.py
    def handle_unauthorized(self, exception):\n    \"\"\"\n    Handles the exception for unauthorized page. By default, it mounts the self.unauthorized_page class and passes it\n    the exception as an argument.\n\n    Args:\n        exception (Exception): The exception that occurred.\n    \"\"\"\n    self.mount_page(\n        self._selector_or_element, self.unauthorized_page, None, {\"error\": exception}, handle_exceptions=False\n    )\n
    "},{"location":"reference/application/#puepy.Application.install_router","title":"install_router(router_class, **kwargs)","text":"

    Install a router in the application.

    Parameters:

    Name Type Description Default router_class class

    A class that implements the router logic for the application. At this time, only puepy.router.Router is available.

    required **kwargs

    Additional keyword arguments that can be passed to the router_class constructor.

    {} Source code in puepy/application.py
    def install_router(self, router_class, **kwargs):\n    \"\"\"\n    Install a router in the application.\n\n    Args:\n        router_class (class): A class that implements the router logic for the application. At this time, only\n            `puepy.router.Router` is available.\n        **kwargs: Additional keyword arguments that can be passed to the router_class constructor.\n    \"\"\"\n    self.router = router_class(application=self, **kwargs)\n    if not is_server_side:\n        add_event_listener(window, \"popstate\", self._on_popstate)\n
    "},{"location":"reference/application/#puepy.Application.mount","title":"mount(selector_or_element, path=None, page_kwargs=None)","text":"

    Mounts a page onto the specified selector or element with optional path and page_kwargs.

    Parameters:

    Name Type Description Default selector_or_element

    The selector or element on which to mount the page.

    required path

    Optional path to match against the router. Defaults to None.

    None page_kwargs

    Optional keyword arguments to pass to the mounted page. Defaults to None.

    None

    Returns:

    Type Description Page

    The mounted page instance

    Source code in puepy/application.py
    def mount(self, selector_or_element, path=None, page_kwargs=None):\n    \"\"\"\n    Mounts a page onto the specified selector or element with optional path and page_kwargs.\n\n    Args:\n        selector_or_element: The selector or element on which to mount the page.\n        path: Optional path to match against the router. Defaults to None.\n        page_kwargs: Optional keyword arguments to pass to the mounted page. Defaults to None.\n\n    Returns:\n        (Page): The mounted page instance\n    \"\"\"\n    if page_kwargs is None:\n        page_kwargs = {}\n\n    self._selector_or_element = selector_or_element\n\n    if self.router:\n        path = path or self.current_path\n        route, arguments = self.router.match(path)\n        if arguments:\n            page_kwargs.update(arguments)\n\n        if route:\n            page_class = route.page\n        elif path in (\"\", \"/\") and self.default_page:\n            page_class = self.default_page\n        elif self.not_found_page:\n            page_class = self.not_found_page\n        else:\n            return None\n    elif self.default_page:\n        route = None\n        page_class = self.default_page\n    else:\n        return None\n\n    self.active_page = None\n    try:\n        self.mount_page(\n            selector_or_element=selector_or_element,\n            page_class=page_class,\n            route=route,\n            page_kwargs=page_kwargs,\n            handle_exceptions=True,\n        )\n    except Exception as e:\n        self.handle_error(e)\n    return self.active_page\n
    "},{"location":"reference/application/#puepy.Application.mount_page","title":"mount_page(selector_or_element, page_class, route, page_kwargs, handle_exceptions=True)","text":"

    Mounts a page on the specified selector or element with the given parameters.

    Parameters:

    Name Type Description Default selector_or_element str or Element

    The selector string or element to mount the page on.

    required page_class class

    The page class to mount.

    required route str

    The route for the page.

    required page_kwargs dict

    Additional keyword arguments to pass to the page class.

    required handle_exceptions bool

    Determines whether to handle exceptions thrown during mounting. Defaults to True.

    True Source code in puepy/application.py
    def mount_page(self, selector_or_element, page_class, route, page_kwargs, handle_exceptions=True):\n    \"\"\"\n    Mounts a page on the specified selector or element with the given parameters.\n\n    Args:\n        selector_or_element (str or Element): The selector string or element to mount the page on.\n        page_class (class): The page class to mount.\n        route (str): The route for the page.\n        page_kwargs (dict): Additional keyword arguments to pass to the page class.\n        handle_exceptions (bool, optional): Determines whether to handle exceptions thrown during mounting.\n            Defaults to True.\n    \"\"\"\n    page_class._expanded_props()\n\n    # For security, we only pass props to the page that are defined in the page's props\n    #\n    # We also handle the list or not-list props for multiple or single values\n    # (eg, ?foo=1&foo=2 -> [\"1\", \"2\"] if needed)\n    #\n    prop_args = {}\n    prop: Prop\n    for prop in page_class.props_expanded.values():\n        if prop.name in page_kwargs:\n            value = page_kwargs.pop(prop.name)\n            if prop.type is list:\n                prop_args[prop.name] = value if isinstance(value, list) else [value]\n            else:\n                prop_args[prop.name] = value if not isinstance(value, list) else value[0]\n\n    self.active_page: Page = page_class(matched_route=route, application=self, extra_args=page_kwargs, **prop_args)\n    try:\n        self.active_page.mount(selector_or_element)\n    except exceptions.PageError as e:\n        if handle_exceptions:\n            self.handle_page_error(e)\n        else:\n            raise\n
    "},{"location":"reference/application/#puepy.Application.page","title":"page(route=None, name=None)","text":"

    A decorator for Page classes which adds the page to the application with a specified route and name.

    Intended to be called as a decorator.

    Parameters:

    Name Type Description Default route str

    The route for the page. Default is None.

    None name str

    The name of the page. If left None, page class is used as the name.

    None

    Examples:

    app = Application()\n@app.page(\"/my-page\")\nclass MyPage(Page):\n    ...\n
    Source code in puepy/application.py
    def page(self, route=None, name=None):\n    \"\"\"\n    A decorator for `Page` classes which adds the page to the application with a specified route and name.\n\n    Intended to be called as a decorator.\n\n    Args:\n        route (str): The route for the page. Default is None.\n        name (str): The name of the page. If left None, page class is used as the name.\n\n    Examples:\n        ``` py\n        app = Application()\n        @app.page(\"/my-page\")\n        class MyPage(Page):\n            ...\n        ```\n    \"\"\"\n    if route:\n        if not self.router:\n            raise Exception(\"Router not installed\")\n\n        def decorator(func):\n            self.router.add_route(route, func, name=name)\n            return func\n\n        return decorator\n    else:\n\n        def decorator(func):\n            self.default_page = func\n            return func\n\n        return decorator\n
    "},{"location":"reference/application/#puepy.Application.remount","title":"remount(path=None, page_kwargs=None)","text":"

    Remounts the selected element or selector with the specified path and page_kwargs.

    Parameters:

    Name Type Description Default path str

    The new path to be used for remounting the element or selector. Default is None.

    None page_kwargs dict

    Additional page kwargs to be passed when remounting. Default is None.

    None Source code in puepy/application.py
    def remount(self, path=None, page_kwargs=None):\n    \"\"\"\n    Remounts the selected element or selector with the specified path and page_kwargs.\n\n    Args:\n        path (str): The new path to be used for remounting the element or selector. Default is None.\n        page_kwargs (dict): Additional page kwargs to be passed when remounting. Default is None.\n\n    \"\"\"\n    self.mount(self._selector_or_element, path=path, page_kwargs=page_kwargs)\n
    "},{"location":"reference/component/","title":"puepy.Component","text":"

    Components should not be created directly

    In your populate() method, call t.tag_name() to create a component. There's no reason an application develop should directly instanciate a component instance and doing so is not supported.

    See also

    • Tutorial on Components
    • In-Depth Components Guide

    Bases: Tag, Stateful

    Components are a way of defining reusable and composable elements in PuePy. They are a subclass of Tag, but provide additional features such as state management and props. By defining your own components and registering them, you can create a library of reusable elements for your application.

    Attributes:

    Name Type Description enclosing_tag str

    The tag name that will enclose the component. To be defined as a class attribute on subclasses.

    component_name str

    The name of the component. If left blank, class name is used. To be defined as a class attribute on subclasses.

    redraw_on_state_changes bool

    Whether the component should redraw when its state changes. To be defined as a class attribute on subclasses.

    redraw_on_app_state_changes bool

    Whether the component should redraw when the application state changes. To be defined as a class attribute on subclasses.

    props list

    A list of props for the component. To be defined as a class attribute on subclasses.

    Source code in puepy/core.py
    class Component(Tag, Stateful):\n    \"\"\"\n    Components are a way of defining reusable and composable elements in PuePy. They are a subclass of Tag, but provide\n    additional features such as state management and props. By defining your own components and registering them, you\n    can create a library of reusable elements for your application.\n\n    Attributes:\n        enclosing_tag (str): The tag name that will enclose the component. To be defined as a class attribute on subclasses.\n        component_name (str): The name of the component. If left blank, class name is used. To be defined as a class attribute on subclasses.\n        redraw_on_state_changes (bool): Whether the component should redraw when its state changes. To be defined as a class attribute on subclasses.\n        redraw_on_app_state_changes (bool): Whether the component should redraw when the application state changes. To be defined as a class attribute on subclasses.\n        props (list): A list of props for the component. To be defined as a class attribute on subclasses.\n    \"\"\"\n\n    enclosing_tag = \"div\"\n    component_name = None\n    redraw_on_state_changes = True\n    redraw_on_app_state_changes = True\n\n    props = []\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, tag_name=self.enclosing_tag, **kwargs)\n        self.state = ReactiveDict(self.initial())\n        self.add_context(\"state\", self.state)\n\n        self.slots = {}\n\n    def _handle_attrs(self, kwargs):\n        self._handle_props(kwargs)\n\n        super()._handle_attrs(kwargs)\n\n    def _handle_props(self, kwargs):\n        if not hasattr(self, \"props_expanded\"):\n            self._expanded_props()\n\n        self.props_values = {}\n        for name, prop in self.props_expanded.items():\n            value = kwargs.pop(prop.name, prop.default_value)\n            setattr(self, name, value)\n            self.props_values[name] = value\n\n    @classmethod\n    def _expanded_props(cls):\n        # This would be ideal for metaprogramming, but we do it this way to be compatible with Micropython. :/\n        props_expanded = {}\n        for prop in cls.props:\n            if isinstance(prop, Prop):\n                props_expanded[prop.name] = prop\n            elif isinstance(prop, dict):\n                props_expanded[prop[\"name\"]] = Prop(**prop)\n            elif isinstance(prop, str):\n                props_expanded[prop] = Prop(name=prop)\n            else:\n                raise PropsError(f\"Unknown prop type {type(prop)}\")\n        cls.props_expanded = props_expanded\n\n    def initial(self):\n        \"\"\"\n        To be overridden in subclasses, the `initial()` method defines the initial state of the component.\n\n        Returns:\n            (dict): Initial component state\n        \"\"\"\n        return {}\n\n    def _on_state_change(self, context, key, value):\n        super()._on_state_change(context, key, value)\n\n        if context == \"state\":\n            redraw_rule = self.redraw_on_state_changes\n        elif context == \"app\":\n            redraw_rule = self.redraw_on_app_state_changes\n        else:\n            return\n\n        if redraw_rule is True:\n            self.page.redraw_tag(self)\n        elif redraw_rule is False:\n            pass\n        elif isinstance(redraw_rule, (list, set)):\n            if key in redraw_rule:\n                self.page.redraw_tag(self)\n        else:\n            raise Exception(f\"Unknown value for redraw rule: {redraw_rule} (context: {context})\")\n\n    def insert_slot(self, name=\"default\", **kwargs):\n        \"\"\"\n        In defining your own component, when you want to create a slot in your `populate` method, you can use this method.\n\n        Args:\n            name (str): The name of the slot. If not passed, the default slot is inserted.\n            **kwargs: Additional keyword arguments to be passed to Slot initialization.\n\n        Returns:\n            Slot: The inserted slot object.\n        \"\"\"\n        if name in self.slots:\n            self.slots[name].parent = Tag.stack[-1]  # The children will be cleared during redraw, so re-establish\n        else:\n            self.slots[name] = Slot(ref=f\"slot={name}\", slot_name=name, page=self.page, parent=Tag.stack[-1], **kwargs)\n        slot = self.slots[name]\n        if self.origin:\n            slot.origin = self.origin\n            if slot.ref:\n                self.origin.refs[slot.ref] = slot\n        return slot\n\n    def slot(self, name=\"default\"):\n        \"\"\"\n        To be used in the `populate` method of code making use of this component, this method returns the slot object\n        with the given name. It should be used inside of a context manager.\n\n        Args:\n            name (str): The name of the slot to clear and return.\n\n        Returns:\n            Slot: The cleared slot object.\n        \"\"\"\n        #\n        # We put this here, so it clears the children only when the slot-filler is doing its filling.\n        # Otherwise, the previous children are kept. Lucky them.\n        self.slots[name].children = []\n        return self.slots[name]\n\n    def __enter__(self):\n        self.stack.append(self)\n        self.origin_stack[0].append(self)\n        self.component_stack.append(self)\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.stack.pop()\n        self.origin_stack[0].pop()\n        self.component_stack.pop()\n        return False\n\n    def __str__(self):\n        return f\"{self.component_name or self.__class__.__name__} ({self.ref} {id(self)})\"\n\n    def __repr__(self):\n        return f\"<{self}>\"\n
    "},{"location":"reference/component/#puepy.Component.initial","title":"initial()","text":"

    To be overridden in subclasses, the initial() method defines the initial state of the component.

    Returns:

    Type Description dict

    Initial component state

    Source code in puepy/core.py
    def initial(self):\n    \"\"\"\n    To be overridden in subclasses, the `initial()` method defines the initial state of the component.\n\n    Returns:\n        (dict): Initial component state\n    \"\"\"\n    return {}\n
    "},{"location":"reference/component/#puepy.Component.insert_slot","title":"insert_slot(name='default', **kwargs)","text":"

    In defining your own component, when you want to create a slot in your populate method, you can use this method.

    Parameters:

    Name Type Description Default name str

    The name of the slot. If not passed, the default slot is inserted.

    'default' **kwargs

    Additional keyword arguments to be passed to Slot initialization.

    {}

    Returns:

    Name Type Description Slot

    The inserted slot object.

    Source code in puepy/core.py
    def insert_slot(self, name=\"default\", **kwargs):\n    \"\"\"\n    In defining your own component, when you want to create a slot in your `populate` method, you can use this method.\n\n    Args:\n        name (str): The name of the slot. If not passed, the default slot is inserted.\n        **kwargs: Additional keyword arguments to be passed to Slot initialization.\n\n    Returns:\n        Slot: The inserted slot object.\n    \"\"\"\n    if name in self.slots:\n        self.slots[name].parent = Tag.stack[-1]  # The children will be cleared during redraw, so re-establish\n    else:\n        self.slots[name] = Slot(ref=f\"slot={name}\", slot_name=name, page=self.page, parent=Tag.stack[-1], **kwargs)\n    slot = self.slots[name]\n    if self.origin:\n        slot.origin = self.origin\n        if slot.ref:\n            self.origin.refs[slot.ref] = slot\n    return slot\n
    "},{"location":"reference/component/#puepy.Component.slot","title":"slot(name='default')","text":"

    To be used in the populate method of code making use of this component, this method returns the slot object with the given name. It should be used inside of a context manager.

    Parameters:

    Name Type Description Default name str

    The name of the slot to clear and return.

    'default'

    Returns:

    Name Type Description Slot

    The cleared slot object.

    Source code in puepy/core.py
    def slot(self, name=\"default\"):\n    \"\"\"\n    To be used in the `populate` method of code making use of this component, this method returns the slot object\n    with the given name. It should be used inside of a context manager.\n\n    Args:\n        name (str): The name of the slot to clear and return.\n\n    Returns:\n        Slot: The cleared slot object.\n    \"\"\"\n    #\n    # We put this here, so it clears the children only when the slot-filler is doing its filling.\n    # Otherwise, the previous children are kept. Lucky them.\n    self.slots[name].children = []\n    return self.slots[name]\n
    "},{"location":"reference/exceptions/","title":"peupy.exceptions","text":"

    Common exceptions in the PuePy framework.

    Classes:

    Name Description ElementNotInDom

    Raised when an element is not found in the DOM, but it is expected to be, such as when getting Tag.element

    PropsError

    Raised when unexpected props are passed to a component

    PageError

    Analogous to http errors, but for a single-page app where the error is client-side

    NotFound

    Page not found

    Forbidden

    Forbidden

    Unauthorized

    Unauthorized

    Redirect

    Redirect

    "},{"location":"reference/exceptions/#puepy.exceptions.ElementNotInDom","title":"ElementNotInDom","text":"

    Bases: Exception

    Raised when an element is not found in the DOM, but it is expected to be, such as when getting Tag.element

    Source code in puepy/exceptions.py
    class ElementNotInDom(Exception):\n    \"\"\"\n    Raised when an element is not found in the DOM, but it is expected to be, such as when getting Tag.element\n    \"\"\"\n\n    pass\n
    "},{"location":"reference/exceptions/#puepy.exceptions.Forbidden","title":"Forbidden","text":"

    Bases: PageError

    Raised manually, presumably when the user is not authorized to access a page.

    Source code in puepy/exceptions.py
    class Forbidden(PageError):\n    \"\"\"\n    Raised manually, presumably when the user is not authorized to access a page.\n    \"\"\"\n\n    def __str__(self):\n        return \"Forbidden\"\n
    "},{"location":"reference/exceptions/#puepy.exceptions.NotFound","title":"NotFound","text":"

    Bases: PageError

    Raised when the router could not find a page matching the user's URL.

    Source code in puepy/exceptions.py
    class NotFound(PageError):\n    \"\"\"\n    Raised when the router could not find a page matching the user's URL.\n    \"\"\"\n\n    def __str__(self):\n        return \"Page not found\"\n
    "},{"location":"reference/exceptions/#puepy.exceptions.PageError","title":"PageError","text":"

    Bases: Exception

    Analogous to http errors, but for a single-page app where the error is client-side

    Source code in puepy/exceptions.py
    class PageError(Exception):\n    \"\"\"\n    Analogous to http errors, but for a single-page app where the error is client-side\n    \"\"\"\n\n    pass\n
    "},{"location":"reference/exceptions/#puepy.exceptions.PropsError","title":"PropsError","text":"

    Bases: ValueError

    Raised when unexpected props are passed to a component

    Source code in puepy/exceptions.py
    class PropsError(ValueError):\n    \"\"\"\n    Raised when unexpected props are passed to a component\n    \"\"\"\n
    "},{"location":"reference/exceptions/#puepy.exceptions.Redirect","title":"Redirect","text":"

    Bases: PageError

    Raised manually when the user should be redirected to another page.

    Source code in puepy/exceptions.py
    class Redirect(PageError):\n    \"\"\"\n    Raised manually when the user should be redirected to another page.\n    \"\"\"\n\n    def __init__(self, path):\n        self.path = path\n\n    def __str__(self):\n        return f\"Redirect to {self.path}\"\n
    "},{"location":"reference/exceptions/#puepy.exceptions.Unauthorized","title":"Unauthorized","text":"

    Bases: PageError

    Raised manually, presumably when the user is not authenticated.

    Source code in puepy/exceptions.py
    class Unauthorized(PageError):\n    \"\"\"\n    Raised manually, presumably when the user is not authenticated.\n    \"\"\"\n\n    def __str__(self):\n        return \"Unauthorized\"\n
    "},{"location":"reference/prop/","title":"puepy.Prop","text":"

    Class representing a prop for a component.

    Attributes:

    Name Type Description name str

    The name of the property.

    description str

    The description of the property (optional).

    type type

    The data type of the property (default: str).

    default_value

    The default value of the property (optional).

    Source code in puepy/core.py
    class Prop:\n    \"\"\"\n    Class representing a prop for a component.\n\n    Attributes:\n        name (str): The name of the property.\n        description (str): The description of the property (optional).\n        type (type): The data type of the property (default: str).\n        default_value: The default value of the property (optional).\n    \"\"\"\n\n    def __init__(self, name, description=None, type=str, default_value=None):\n        self.name = name\n        self.description = description\n        self.type = type\n        self.default_value = default_value\n
    "},{"location":"reference/reactivity/","title":"puepy.reactivity","text":"

    Provides the base classes for PuePy's reactivity system independent of web concerns. These classes are not intended to be used directly, but could be useful for implementing a similar system in a different context.

    Classes:

    Name Description Listener

    A simple class that notifies a collection of callback functions when its notify method is called

    ReactiveDict

    A dictionary that notifies a listener when it is updated

    "},{"location":"reference/reactivity/#puepy.reactivity.Listener","title":"Listener","text":"

    A simple class that allows you to register callbacks and then notify them all at once.

    Attributes:

    Name Type Description callbacks list of callables

    A list of callback functions to be called when notify is called

    Source code in puepy/reactivity.py
    class Listener:\n    \"\"\"\n    A simple class that allows you to register callbacks and then notify them all at once.\n\n    Attributes:\n        callbacks (list of callables): A list of callback functions to be called when `notify` is called\n    \"\"\"\n\n    def __init__(self):\n        self.callbacks = []\n\n    def add_callback(self, callback):\n        \"\"\"\n        Adds a callback function to the listener.\n\n        Args:\n            callback (callable): The callback function to be added\n        \"\"\"\n        self.callbacks.append(callback)\n\n    def remove_callback(self, callback):\n        \"\"\"\n        Removes a callback function from the listener.\n\n        Args:\n            callback (callable): The callback to be removed\n        \"\"\"\n        self.callbacks.remove(callback)\n\n    def notify(self, *args, **kwargs):\n        \"\"\"\n        Notify method\n\n        Executes each callback function in the callbacks list by passing in the given arguments and keyword arguments.\n        If an exception occurs during the callback execution, it is logged using the logging library.\n\n        Args:\n            *args: Variable length argument list.\n            **kwargs: Arbitrary keyword arguments.\n        \"\"\"\n        for callback in self.callbacks:\n            try:\n                callback(*args, **kwargs)\n            except Exception as e:\n                logging.exception(\"Error in callback for {self}: {callback}:\".format(self=self, callback=callback))\n\n    def __str__(self):\n        if len(self.callbacks) == 1:\n            return f\"Listener: {self.callbacks[0]}\"\n        elif len(self.callbacks) > 1:\n            return f\"Listener with {len(self.callbacks)} callbacks\"\n        else:\n            return \"Listener with no callbacks\"\n\n    def __repr__(self):\n        return f\"<{self}>\"\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Listener.add_callback","title":"add_callback(callback)","text":"

    Adds a callback function to the listener.

    Parameters:

    Name Type Description Default callback callable

    The callback function to be added

    required Source code in puepy/reactivity.py
    def add_callback(self, callback):\n    \"\"\"\n    Adds a callback function to the listener.\n\n    Args:\n        callback (callable): The callback function to be added\n    \"\"\"\n    self.callbacks.append(callback)\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Listener.notify","title":"notify(*args, **kwargs)","text":"

    Notify method

    Executes each callback function in the callbacks list by passing in the given arguments and keyword arguments. If an exception occurs during the callback execution, it is logged using the logging library.

    Parameters:

    Name Type Description Default *args

    Variable length argument list.

    () **kwargs

    Arbitrary keyword arguments.

    {} Source code in puepy/reactivity.py
    def notify(self, *args, **kwargs):\n    \"\"\"\n    Notify method\n\n    Executes each callback function in the callbacks list by passing in the given arguments and keyword arguments.\n    If an exception occurs during the callback execution, it is logged using the logging library.\n\n    Args:\n        *args: Variable length argument list.\n        **kwargs: Arbitrary keyword arguments.\n    \"\"\"\n    for callback in self.callbacks:\n        try:\n            callback(*args, **kwargs)\n        except Exception as e:\n            logging.exception(\"Error in callback for {self}: {callback}:\".format(self=self, callback=callback))\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Listener.remove_callback","title":"remove_callback(callback)","text":"

    Removes a callback function from the listener.

    Parameters:

    Name Type Description Default callback callable

    The callback to be removed

    required Source code in puepy/reactivity.py
    def remove_callback(self, callback):\n    \"\"\"\n    Removes a callback function from the listener.\n\n    Args:\n        callback (callable): The callback to be removed\n    \"\"\"\n    self.callbacks.remove(callback)\n
    "},{"location":"reference/reactivity/#puepy.reactivity.ReactiveDict","title":"ReactiveDict","text":"

    Bases: dict

    A dictionary that notifies a listener when it is updated.

    Attributes:

    Name Type Description listener Listener

    A listener object that is notified when the dictionary is updated

    key_listeners dict

    A dictionary of listeners that are notified when a specific key is updated

    Source code in puepy/reactivity.py
    class ReactiveDict(dict):\n    \"\"\"\n    A dictionary that notifies a listener when it is updated.\n\n    Attributes:\n        listener (Listener): A listener object that is notified when the dictionary is updated\n        key_listeners (dict): A dictionary of listeners that are notified when a specific key is updated\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args)\n        self.listener = Listener()\n        self.key_listeners = {}\n        self._in_mutation = False\n        self._notifications_pending = set()\n        self._keys_mutate = None\n\n    def add_key_listener(self, key, callback):\n        \"\"\"\n        Adds a key listener to the object.\n\n        Args:\n            key (str): The key for which the listener will be added.\n            callback (callable): The callback function to be executed when the key event is triggered.\n        \"\"\"\n        if key not in self.key_listeners:\n            self.key_listeners[key] = Listener()\n        self.key_listeners[key].add_callback(callback)\n\n    def notify(self, *keys):\n        \"\"\"\n        Notifies the listener and key listeners that the object has been updated.\n\n        Args:\n            *keys: A variable number of keys to be modified for key-specific listeners.\n        \"\"\"\n        if keys:\n            self._notifications_pending.update(keys)\n        else:\n            self._notifications_pending.update(self.keys())\n\n        if not self._in_mutation:\n            self._flush_pending()\n\n    def mutate(self, *keys):\n        \"\"\"\n        To be used as a context manager, this method is for either deferring all notifications until a change has been completed and/or notifying listeners when \"deep\" changes are made that would have gone undetected by `__setitem__`.\n\n        Examples:\n            ``` py\n            with reactive_dict.mutate(\"my_list\", \"my_dict\"):\n                reactive_dict[\"my_list\"].append(\"spam\")\n                reactive_dict[\"my_dict\"][\"spam\"] = \"eggs\"\n            ```\n\n        Args:\n            *keys: A variable number of keys to update the notifications pending attribute with. If no keys are provided, all keys in the object will be updated.\n\n        Returns:\n            The reactive dict itself, which stylistically could be nice to use in a `with` statement.\n        \"\"\"\n        if keys:\n            self._notifications_pending.update(keys)\n        else:\n            self._notifications_pending.update(self.keys())\n        self._keys_mutate = keys\n        return self\n\n    def update(self, other):\n        with self.mutate(*other.keys()):\n            super().update(other)\n\n    def _flush_pending(self):\n        while self._notifications_pending:\n            key = self._notifications_pending.pop()\n            value = self.get(key, None)\n            self.listener.notify(key, value)\n            if key in self.key_listeners:\n                self.key_listeners[key].notify(key, value)\n\n    def __setitem__(self, key, value):\n        if (key in self and value != self[key]) or key not in self:\n            super().__setitem__(key, value)\n            self.notify(key)\n\n    def __delitem__(self, key):\n        super().__delitem__(key)\n        self.notify(key)\n\n    def __enter__(self):\n        self._in_mutation = True\n\n        if len(self._keys_mutate) == 0:\n            return self.get(self._keys_mutate)\n        elif len(self._keys_mutate) > 1:\n            return [self.get(k) for k in self._keys_mutate]\n\n    def __exit__(self, type, value, traceback):\n        self._in_mutation = False\n        self._flush_pending()\n
    "},{"location":"reference/reactivity/#puepy.reactivity.ReactiveDict.add_key_listener","title":"add_key_listener(key, callback)","text":"

    Adds a key listener to the object.

    Parameters:

    Name Type Description Default key str

    The key for which the listener will be added.

    required callback callable

    The callback function to be executed when the key event is triggered.

    required Source code in puepy/reactivity.py
    def add_key_listener(self, key, callback):\n    \"\"\"\n    Adds a key listener to the object.\n\n    Args:\n        key (str): The key for which the listener will be added.\n        callback (callable): The callback function to be executed when the key event is triggered.\n    \"\"\"\n    if key not in self.key_listeners:\n        self.key_listeners[key] = Listener()\n    self.key_listeners[key].add_callback(callback)\n
    "},{"location":"reference/reactivity/#puepy.reactivity.ReactiveDict.mutate","title":"mutate(*keys)","text":"

    To be used as a context manager, this method is for either deferring all notifications until a change has been completed and/or notifying listeners when \"deep\" changes are made that would have gone undetected by __setitem__.

    Examples:

    with reactive_dict.mutate(\"my_list\", \"my_dict\"):\n    reactive_dict[\"my_list\"].append(\"spam\")\n    reactive_dict[\"my_dict\"][\"spam\"] = \"eggs\"\n

    Parameters:

    Name Type Description Default *keys

    A variable number of keys to update the notifications pending attribute with. If no keys are provided, all keys in the object will be updated.

    ()

    Returns:

    Type Description

    The reactive dict itself, which stylistically could be nice to use in a with statement.

    Source code in puepy/reactivity.py
    def mutate(self, *keys):\n    \"\"\"\n    To be used as a context manager, this method is for either deferring all notifications until a change has been completed and/or notifying listeners when \"deep\" changes are made that would have gone undetected by `__setitem__`.\n\n    Examples:\n        ``` py\n        with reactive_dict.mutate(\"my_list\", \"my_dict\"):\n            reactive_dict[\"my_list\"].append(\"spam\")\n            reactive_dict[\"my_dict\"][\"spam\"] = \"eggs\"\n        ```\n\n    Args:\n        *keys: A variable number of keys to update the notifications pending attribute with. If no keys are provided, all keys in the object will be updated.\n\n    Returns:\n        The reactive dict itself, which stylistically could be nice to use in a `with` statement.\n    \"\"\"\n    if keys:\n        self._notifications_pending.update(keys)\n    else:\n        self._notifications_pending.update(self.keys())\n    self._keys_mutate = keys\n    return self\n
    "},{"location":"reference/reactivity/#puepy.reactivity.ReactiveDict.notify","title":"notify(*keys)","text":"

    Notifies the listener and key listeners that the object has been updated.

    Parameters:

    Name Type Description Default *keys

    A variable number of keys to be modified for key-specific listeners.

    () Source code in puepy/reactivity.py
    def notify(self, *keys):\n    \"\"\"\n    Notifies the listener and key listeners that the object has been updated.\n\n    Args:\n        *keys: A variable number of keys to be modified for key-specific listeners.\n    \"\"\"\n    if keys:\n        self._notifications_pending.update(keys)\n    else:\n        self._notifications_pending.update(self.keys())\n\n    if not self._in_mutation:\n        self._flush_pending()\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Stateful","title":"Stateful","text":"

    A class that provides a reactive state management system for components. A

    Source code in puepy/reactivity.py
    class Stateful:\n    \"\"\"\n    A class that provides a reactive state management system for components. A\n    \"\"\"\n\n    def add_context(self, name: str, value: ReactiveDict):\n        \"\"\"\n        Adds contxt from a reactive dict to be reacted on by the component.\n        \"\"\"\n        value.listener.add_callback(partial(self._on_state_change, name))\n\n    def initial(self):\n        \"\"\"\n        To be overridden in subclasses, the `initial()` method defines the initial state of the stateful object.\n\n        Returns:\n            (dict): Initial component state\n        \"\"\"\n        return {}\n\n    def on_state_change(self, context, key, value):\n        \"\"\"\n        To be overridden in subclasses, this method is called whenever the state of the component changes.\n\n        Args:\n            context: What context the state change occured in\n            key: The key modified\n            value: The new value\n        \"\"\"\n        pass\n\n    def _on_state_change(self, context, key, value):\n        self.on_state_change(context, key, value)\n\n        if hasattr(self, f\"on_{key}_change\"):\n            getattr(self, f\"on_{key}_change\")(value)\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Stateful.add_context","title":"add_context(name, value)","text":"

    Adds contxt from a reactive dict to be reacted on by the component.

    Source code in puepy/reactivity.py
    def add_context(self, name: str, value: ReactiveDict):\n    \"\"\"\n    Adds contxt from a reactive dict to be reacted on by the component.\n    \"\"\"\n    value.listener.add_callback(partial(self._on_state_change, name))\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Stateful.initial","title":"initial()","text":"

    To be overridden in subclasses, the initial() method defines the initial state of the stateful object.

    Returns:

    Type Description dict

    Initial component state

    Source code in puepy/reactivity.py
    def initial(self):\n    \"\"\"\n    To be overridden in subclasses, the `initial()` method defines the initial state of the stateful object.\n\n    Returns:\n        (dict): Initial component state\n    \"\"\"\n    return {}\n
    "},{"location":"reference/reactivity/#puepy.reactivity.Stateful.on_state_change","title":"on_state_change(context, key, value)","text":"

    To be overridden in subclasses, this method is called whenever the state of the component changes.

    Parameters:

    Name Type Description Default context

    What context the state change occured in

    required key

    The key modified

    required value

    The new value

    required Source code in puepy/reactivity.py
    def on_state_change(self, context, key, value):\n    \"\"\"\n    To be overridden in subclasses, this method is called whenever the state of the component changes.\n\n    Args:\n        context: What context the state change occured in\n        key: The key modified\n        value: The new value\n    \"\"\"\n    pass\n
    "},{"location":"reference/router/","title":"puepy.router","text":"

    The puepy.router module contains code relevant to optional client-side routing in PuePy.

    See Also

    • Tutorial: Routing
    • Guide: Advanced Routing

    PuePy's router functionality can be optionally installed by calling the install_router method of the Application class.

    Example
    from puepy import Application, Router\n\napp = Application()\napp.install_router(Router, link_mode=Router.LINK_MODE_HASH)\n

    Once installed, the Router instance is available on app.Router and can be used throughout the application to manage client-side routing. Routes are defined by either using the @app.page decorator or by calling methods manually on the Router instance.

    Classes:

    Name Description puepy.router.Route

    Represents a route in the router.

    puepy.router.Router

    Represents a router for managing client-side routing in a web application.

    "},{"location":"reference/router/#puepy.router.Route","title":"Route","text":"

    Represents a route in the router. A route is defined by a path match pattern, a page class, and a name.

    Note

    This is usually not instanciated directly. Instead, use the Router.add_route method to create a new route or use the @app.page decorator to define a route at the time you define your Pages.

    Source code in puepy/router.py
    class Route:\n    \"\"\"\n    Represents a route in the router. A route is defined by a path match pattern, a page class, and a name.\n\n    Note:\n        This is usually not instanciated directly. Instead, use the `Router.add_route` method to create a new route or\n        use the @app.page decorator to define a route at the time you define your Pages.\n    \"\"\"\n\n    def __init__(self, path_match: str, page: Page, name: str, base_path: str, router=None):\n        \"\"\"\n        Args:\n            path_match (str): The path match pattern used for routing.\n            page (Page): An instance of the Page class representing the page.\n            name (str): The name of the page.\n            base_path (str): The base path used for routing.\n            router (Router, optional): An optional parameter representing the router used for routing.\n        \"\"\"\n        self.path_match = path_match\n        self.page = page\n        self.name = name\n        self.base_path = base_path\n        self.router = router\n\n    def match(self, path):\n        \"\"\"\n        Evaluates a path against the route's pattern to determine if there is a match.\n\n        Args:\n            path: The path to be matched against the pattern.\n\n        Returns:\n            Match found (tuple): A tuple containing a True boolean value and a dictionary. The dictionary contains the\n            matched variables extracted from the path.\n\n            Match not found (tuple): If no match is found, returns `(False, None)`.\n        \"\"\"\n        if self.base_path and path.startswith(self.base_path):\n            path = path[len(self.base_path) :]\n\n        # Simple pattern matching without regex\n        parts = path.strip(\"/\").split(\"/\")\n        pattern_parts = self.path_match.strip(\"/\").split(\"/\")\n        if len(parts) != len(pattern_parts):\n            return False, None\n\n        kwargs = {}\n        for part, pattern_part in zip(parts, pattern_parts):\n            if pattern_part.startswith(\"<\") and pattern_part.endswith(\">\"):\n                group_name = pattern_part[1:-1]\n                kwargs[group_name] = part\n            elif part != pattern_part:\n                return False, None\n\n        return True, kwargs\n\n    def reverse(self, **kwargs):\n        \"\"\"\n        Reverse method is used to generate a URL path using the given parameters. It replaces the placeholders in the\n        path template with the corresponding values.\n\n        Args:\n            **kwargs: A variable number of keyword arguments representing the values to be inserted into the path\n            template.\n\n        Returns:\n            (str): The generated URL path.\n\n        Example:\n            Let's say we have a path template `/users/<username>/posts/<post_id>`. We can use the reverse method to\n            generate the URL path by providing the values for \"username\" and \"post_id\" as keyword arguments:\n            `route.reverse(username=\"john\", post_id=123)` => `\"/users/john/posts/123\"`\n        \"\"\"\n        kwargs = kwargs.copy()\n        result = self.path_match\n        for key in list(kwargs.keys()):\n            if f\"<{key}>\" in result:\n                value = kwargs.pop(key)\n                result = result.replace(f\"<{key}>\", str(value))\n\n        if self.router and self.router.link_mode == Router.LINK_MODE_HASH:\n            result = \"#\" + result\n\n        if self.base_path:\n            path = f\"{self.base_path}{result}\"\n        else:\n            path = result\n\n        if kwargs:\n            path += \"?\" + \"&\".join(f\"{url_quote(k)}={url_quote(v)}\" for k, v in kwargs.items())\n        return path\n\n    def __str__(self):\n        return self.name\n\n    def __repr__(self):\n        return f\"<Route: {self.name}>\"\n
    "},{"location":"reference/router/#puepy.router.Route.__init__","title":"__init__(path_match, page, name, base_path, router=None)","text":"

    Parameters:

    Name Type Description Default path_match str

    The path match pattern used for routing.

    required page Page

    An instance of the Page class representing the page.

    required name str

    The name of the page.

    required base_path str

    The base path used for routing.

    required router Router

    An optional parameter representing the router used for routing.

    None Source code in puepy/router.py
    def __init__(self, path_match: str, page: Page, name: str, base_path: str, router=None):\n    \"\"\"\n    Args:\n        path_match (str): The path match pattern used for routing.\n        page (Page): An instance of the Page class representing the page.\n        name (str): The name of the page.\n        base_path (str): The base path used for routing.\n        router (Router, optional): An optional parameter representing the router used for routing.\n    \"\"\"\n    self.path_match = path_match\n    self.page = page\n    self.name = name\n    self.base_path = base_path\n    self.router = router\n
    "},{"location":"reference/router/#puepy.router.Route.match","title":"match(path)","text":"

    Evaluates a path against the route's pattern to determine if there is a match.

    Parameters:

    Name Type Description Default path

    The path to be matched against the pattern.

    required

    Returns:

    Type Description

    Match found (tuple): A tuple containing a True boolean value and a dictionary. The dictionary contains the

    matched variables extracted from the path.

    Match not found (tuple): If no match is found, returns (False, None).

    Source code in puepy/router.py
    def match(self, path):\n    \"\"\"\n    Evaluates a path against the route's pattern to determine if there is a match.\n\n    Args:\n        path: The path to be matched against the pattern.\n\n    Returns:\n        Match found (tuple): A tuple containing a True boolean value and a dictionary. The dictionary contains the\n        matched variables extracted from the path.\n\n        Match not found (tuple): If no match is found, returns `(False, None)`.\n    \"\"\"\n    if self.base_path and path.startswith(self.base_path):\n        path = path[len(self.base_path) :]\n\n    # Simple pattern matching without regex\n    parts = path.strip(\"/\").split(\"/\")\n    pattern_parts = self.path_match.strip(\"/\").split(\"/\")\n    if len(parts) != len(pattern_parts):\n        return False, None\n\n    kwargs = {}\n    for part, pattern_part in zip(parts, pattern_parts):\n        if pattern_part.startswith(\"<\") and pattern_part.endswith(\">\"):\n            group_name = pattern_part[1:-1]\n            kwargs[group_name] = part\n        elif part != pattern_part:\n            return False, None\n\n    return True, kwargs\n
    "},{"location":"reference/router/#puepy.router.Route.reverse","title":"reverse(**kwargs)","text":"

    Reverse method is used to generate a URL path using the given parameters. It replaces the placeholders in the path template with the corresponding values.

    Parameters:

    Name Type Description Default **kwargs

    A variable number of keyword arguments representing the values to be inserted into the path

    {}

    Returns:

    Type Description str

    The generated URL path.

    Example

    Let's say we have a path template /users/<username>/posts/<post_id>. We can use the reverse method to generate the URL path by providing the values for \"username\" and \"post_id\" as keyword arguments: route.reverse(username=\"john\", post_id=123) => \"/users/john/posts/123\"

    Source code in puepy/router.py
    def reverse(self, **kwargs):\n    \"\"\"\n    Reverse method is used to generate a URL path using the given parameters. It replaces the placeholders in the\n    path template with the corresponding values.\n\n    Args:\n        **kwargs: A variable number of keyword arguments representing the values to be inserted into the path\n        template.\n\n    Returns:\n        (str): The generated URL path.\n\n    Example:\n        Let's say we have a path template `/users/<username>/posts/<post_id>`. We can use the reverse method to\n        generate the URL path by providing the values for \"username\" and \"post_id\" as keyword arguments:\n        `route.reverse(username=\"john\", post_id=123)` => `\"/users/john/posts/123\"`\n    \"\"\"\n    kwargs = kwargs.copy()\n    result = self.path_match\n    for key in list(kwargs.keys()):\n        if f\"<{key}>\" in result:\n            value = kwargs.pop(key)\n            result = result.replace(f\"<{key}>\", str(value))\n\n    if self.router and self.router.link_mode == Router.LINK_MODE_HASH:\n        result = \"#\" + result\n\n    if self.base_path:\n        path = f\"{self.base_path}{result}\"\n    else:\n        path = result\n\n    if kwargs:\n        path += \"?\" + \"&\".join(f\"{url_quote(k)}={url_quote(v)}\" for k, v in kwargs.items())\n    return path\n
    "},{"location":"reference/router/#puepy.router.Router","title":"Router","text":"

    Class representing a router for managing client-side routing in a web application.

    Parameters:

    Name Type Description Default application object

    The web application object. Defaults to None.

    None base_path str

    The base path URL. Defaults to None.

    None link_mode str

    The link mode for navigating. Defaults to \"hash\".

    LINK_MODE_HASH

    Attributes:

    Name Type Description LINK_MODE_DIRECT str

    Direct link mode.

    LINK_MODE_HTML5 str

    HTML5 link mode.

    LINK_MODE_HASH str

    Hash link mode.

    routes list

    List of Route instances.

    routes_by_name dict

    Dictionary mapping route names to Route instances.

    routes_by_page dict

    Dictionary mapping page classes to Route instances.

    application object

    The web application object.

    base_path str

    The base path URL.

    link_mode str

    The link mode for navigating.

    Source code in puepy/router.py
    class Router:\n    \"\"\"Class representing a router for managing client-side routing in a web application.\n\n\n\n    Args:\n        application (object, optional): The web application object. Defaults to None.\n        base_path (str, optional): The base path URL. Defaults to None.\n        link_mode (str, optional): The link mode for navigating. Defaults to \"hash\".\n\n    Attributes:\n        LINK_MODE_DIRECT (str): Direct link mode.\n        LINK_MODE_HTML5 (str): HTML5 link mode.\n        LINK_MODE_HASH (str): Hash link mode.\n        routes (list): List of Route instances.\n        routes_by_name (dict): Dictionary mapping route names to Route instances.\n        routes_by_page (dict): Dictionary mapping page classes to Route instances.\n        application (object): The web application object.\n        base_path (str): The base path URL.\n        link_mode (str): The link mode for navigating.\n    \"\"\"\n\n    LINK_MODE_DIRECT = \"direct\"\n    LINK_MODE_HTML5 = \"html5\"\n    LINK_MODE_HASH = \"hash\"\n\n    def __init__(self, application=None, base_path=None, link_mode=LINK_MODE_HASH):\n        \"\"\"\n        Initializes an instance of the class.\n\n        Parameters:\n            application (Application): The application used for routing.\n            base_path (str): The base path for the routes.\n            link_mode (str): The mode for generating links.\n        \"\"\"\n        self.routes = []\n        self.routes_by_name = {}\n        self.routes_by_page = {}\n        self.application = application\n        self.base_path = base_path\n        self.link_mode = link_mode\n\n    def add_route_instance(self, route: Route):\n        \"\"\"\n        Add a route instance to the current router.\n\n        Parameters:\n            route (Route): The route instance to be added.\n\n        Raises:\n            ValueError: If the route instance or route name already exists in the router.\n        \"\"\"\n        if route in self.routes:\n            raise ValueError(f\"Route already added: {route}\")\n        if route.name in self.routes_by_name:\n            raise ValueError(f\"Route name already exists for another route: {route.name}\")\n        self.routes.append(route)\n        self.routes_by_name[route.name] = route\n        self.routes_by_page[route.page] = route\n        route.router = self\n\n    def add_route(self, path_match, page_class, name=None):\n        \"\"\"\n        Adds a route to the router. This method creates a new Route instance.\n\n        Args:\n            path_match (str): The URL path pattern to match for the route.\n            page_class (Page class): The class or function to be associated with the route.\n            name (str, optional): The name of the route. If not provided, the name will be derived from the page class name.\n        \"\"\"\n        # Convert path to a simple pattern without regex\n        if not name:\n            name = mixed_to_underscores(page_class.__name__)\n        self.add_route_instance(Route(path_match=path_match, page=page_class, name=name, base_path=self.base_path))\n\n    def reverse(self, destination, **kwargs):\n        \"\"\"\n        Reverses a\n\n        Args:\n            destination: The destination to reverse. It can be the name of a route, the mapped page of a route, or the default page of the application.\n            **kwargs: Additional keyword arguments to be passed to the reverse method of the destination route.\n\n        Returns:\n            (str): The reversed URL for the given destination.\n\n        Raises:\n            KeyError: If the destination is not found in the routes.\n        \"\"\"\n        route: Route\n        if isinstance(destination, Route):\n            return destination.reverse(**kwargs)\n        elif destination in self.routes_by_name:\n            route = self.routes_by_name[destination]\n        elif destination in self.routes_by_page:\n            route = self.routes_by_page[destination]\n        elif self.application and destination is self.application.default_page:\n            if self.link_mode == Router.LINK_MODE_HASH:\n                path = \"#/\"\n            else:\n                path = \"/\"\n            return self.base_path or \"\" + path\n        else:\n            raise KeyError(f\"{destination} not found in routes\")\n        return route.reverse(**kwargs)\n\n    def match(self, path):\n        \"\"\"\n        Args:\n            path (str): The path to be matched.\n\n        Returns:\n            (tuple): A tuple containing the matching route and the matched route arguments (if any). If no route is\n                found, returns (None, None).\n        \"\"\"\n        path = path.split(\"#\")[0]\n        if \"?\" not in path:\n            path += \"?\"\n        path, query_string = path.split(\"?\", 1)\n        arguments = parse_query_string(query_string)\n\n        for route in self.routes:\n            matches, path_arguments = route.match(path)\n            if path_arguments:\n                arguments.update(path_arguments)\n            if matches:\n                return route, arguments\n        return None, None\n\n    def navigate_to_path(self, path, **kwargs):\n        \"\"\"\n        Navigates to the specified path.\n\n        Args:\n            path (str or Page): The path to navigate to. If path is a subclass of Page, it will be reversed using the reverse method\n            provided by the self object. If path is a string and **kwargs is not empty, it will append the query string\n            to the path.\n\n            **kwargs: Additional key-value pairs to be included in the query string. Each key-value pair will be\n            URL-encoded.\n\n        Raises:\n            Exception: If the link mode is invalid.\n        \"\"\"\n        if isinstance(path, type) and issubclass(path, Page):\n            path = self.reverse(path, **kwargs)\n        elif kwargs:\n            path += \"?\" + \"&\".join(f\"{url_quote(k)}={url_quote(v)}\" for k, v in kwargs.items())\n\n        if self.link_mode == self.LINK_MODE_DIRECT:\n            window.location = path\n        elif self.link_mode == self.LINK_MODE_HTML5:\n            history.pushState(jsobj(), \"\", path)\n            self.application.mount(self.application._selector_or_element, path)\n        elif self.link_mode == self.LINK_MODE_HASH:\n            path = path[1:] if path.startswith(\"#\") else path\n            if not is_server_side:\n                history.pushState(jsobj(), \"\", \"#\" + path)\n            self.application.mount(self.application._selector_or_element, path)\n        else:\n            raise Exception(f\"Invalid link mode: {self.link_mode}\")\n
    "},{"location":"reference/router/#puepy.router.Router.__init__","title":"__init__(application=None, base_path=None, link_mode=LINK_MODE_HASH)","text":"

    Initializes an instance of the class.

    Parameters:

    Name Type Description Default application Application

    The application used for routing.

    None base_path str

    The base path for the routes.

    None link_mode str

    The mode for generating links.

    LINK_MODE_HASH Source code in puepy/router.py
    def __init__(self, application=None, base_path=None, link_mode=LINK_MODE_HASH):\n    \"\"\"\n    Initializes an instance of the class.\n\n    Parameters:\n        application (Application): The application used for routing.\n        base_path (str): The base path for the routes.\n        link_mode (str): The mode for generating links.\n    \"\"\"\n    self.routes = []\n    self.routes_by_name = {}\n    self.routes_by_page = {}\n    self.application = application\n    self.base_path = base_path\n    self.link_mode = link_mode\n
    "},{"location":"reference/router/#puepy.router.Router.add_route","title":"add_route(path_match, page_class, name=None)","text":"

    Adds a route to the router. This method creates a new Route instance.

    Parameters:

    Name Type Description Default path_match str

    The URL path pattern to match for the route.

    required page_class Page class

    The class or function to be associated with the route.

    required name str

    The name of the route. If not provided, the name will be derived from the page class name.

    None Source code in puepy/router.py
    def add_route(self, path_match, page_class, name=None):\n    \"\"\"\n    Adds a route to the router. This method creates a new Route instance.\n\n    Args:\n        path_match (str): The URL path pattern to match for the route.\n        page_class (Page class): The class or function to be associated with the route.\n        name (str, optional): The name of the route. If not provided, the name will be derived from the page class name.\n    \"\"\"\n    # Convert path to a simple pattern without regex\n    if not name:\n        name = mixed_to_underscores(page_class.__name__)\n    self.add_route_instance(Route(path_match=path_match, page=page_class, name=name, base_path=self.base_path))\n
    "},{"location":"reference/router/#puepy.router.Router.add_route_instance","title":"add_route_instance(route)","text":"

    Add a route instance to the current router.

    Parameters:

    Name Type Description Default route Route

    The route instance to be added.

    required

    Raises:

    Type Description ValueError

    If the route instance or route name already exists in the router.

    Source code in puepy/router.py
    def add_route_instance(self, route: Route):\n    \"\"\"\n    Add a route instance to the current router.\n\n    Parameters:\n        route (Route): The route instance to be added.\n\n    Raises:\n        ValueError: If the route instance or route name already exists in the router.\n    \"\"\"\n    if route in self.routes:\n        raise ValueError(f\"Route already added: {route}\")\n    if route.name in self.routes_by_name:\n        raise ValueError(f\"Route name already exists for another route: {route.name}\")\n    self.routes.append(route)\n    self.routes_by_name[route.name] = route\n    self.routes_by_page[route.page] = route\n    route.router = self\n
    "},{"location":"reference/router/#puepy.router.Router.match","title":"match(path)","text":"

    Parameters:

    Name Type Description Default path str

    The path to be matched.

    required

    Returns:

    Type Description tuple

    A tuple containing the matching route and the matched route arguments (if any). If no route is found, returns (None, None).

    Source code in puepy/router.py
    def match(self, path):\n    \"\"\"\n    Args:\n        path (str): The path to be matched.\n\n    Returns:\n        (tuple): A tuple containing the matching route and the matched route arguments (if any). If no route is\n            found, returns (None, None).\n    \"\"\"\n    path = path.split(\"#\")[0]\n    if \"?\" not in path:\n        path += \"?\"\n    path, query_string = path.split(\"?\", 1)\n    arguments = parse_query_string(query_string)\n\n    for route in self.routes:\n        matches, path_arguments = route.match(path)\n        if path_arguments:\n            arguments.update(path_arguments)\n        if matches:\n            return route, arguments\n    return None, None\n
    "},{"location":"reference/router/#puepy.router.Router.navigate_to_path","title":"navigate_to_path(path, **kwargs)","text":"

    Navigates to the specified path.

    Parameters:

    Name Type Description Default path str or Page

    The path to navigate to. If path is a subclass of Page, it will be reversed using the reverse method

    required **kwargs

    Additional key-value pairs to be included in the query string. Each key-value pair will be

    {}

    Raises:

    Type Description Exception

    If the link mode is invalid.

    Source code in puepy/router.py
    def navigate_to_path(self, path, **kwargs):\n    \"\"\"\n    Navigates to the specified path.\n\n    Args:\n        path (str or Page): The path to navigate to. If path is a subclass of Page, it will be reversed using the reverse method\n        provided by the self object. If path is a string and **kwargs is not empty, it will append the query string\n        to the path.\n\n        **kwargs: Additional key-value pairs to be included in the query string. Each key-value pair will be\n        URL-encoded.\n\n    Raises:\n        Exception: If the link mode is invalid.\n    \"\"\"\n    if isinstance(path, type) and issubclass(path, Page):\n        path = self.reverse(path, **kwargs)\n    elif kwargs:\n        path += \"?\" + \"&\".join(f\"{url_quote(k)}={url_quote(v)}\" for k, v in kwargs.items())\n\n    if self.link_mode == self.LINK_MODE_DIRECT:\n        window.location = path\n    elif self.link_mode == self.LINK_MODE_HTML5:\n        history.pushState(jsobj(), \"\", path)\n        self.application.mount(self.application._selector_or_element, path)\n    elif self.link_mode == self.LINK_MODE_HASH:\n        path = path[1:] if path.startswith(\"#\") else path\n        if not is_server_side:\n            history.pushState(jsobj(), \"\", \"#\" + path)\n        self.application.mount(self.application._selector_or_element, path)\n    else:\n        raise Exception(f\"Invalid link mode: {self.link_mode}\")\n
    "},{"location":"reference/router/#puepy.router.Router.reverse","title":"reverse(destination, **kwargs)","text":"

    Reverses a

    Parameters:

    Name Type Description Default destination

    The destination to reverse. It can be the name of a route, the mapped page of a route, or the default page of the application.

    required **kwargs

    Additional keyword arguments to be passed to the reverse method of the destination route.

    {}

    Returns:

    Type Description str

    The reversed URL for the given destination.

    Raises:

    Type Description KeyError

    If the destination is not found in the routes.

    Source code in puepy/router.py
    def reverse(self, destination, **kwargs):\n    \"\"\"\n    Reverses a\n\n    Args:\n        destination: The destination to reverse. It can be the name of a route, the mapped page of a route, or the default page of the application.\n        **kwargs: Additional keyword arguments to be passed to the reverse method of the destination route.\n\n    Returns:\n        (str): The reversed URL for the given destination.\n\n    Raises:\n        KeyError: If the destination is not found in the routes.\n    \"\"\"\n    route: Route\n    if isinstance(destination, Route):\n        return destination.reverse(**kwargs)\n    elif destination in self.routes_by_name:\n        route = self.routes_by_name[destination]\n    elif destination in self.routes_by_page:\n        route = self.routes_by_page[destination]\n    elif self.application and destination is self.application.default_page:\n        if self.link_mode == Router.LINK_MODE_HASH:\n            path = \"#/\"\n        else:\n            path = \"/\"\n        return self.base_path or \"\" + path\n    else:\n        raise KeyError(f\"{destination} not found in routes\")\n    return route.reverse(**kwargs)\n
    "},{"location":"reference/router/#puepy.router._micropython_parse_query_string","title":"_micropython_parse_query_string(query_string)","text":"

    In MicroPython, urllib isn't available and we can't use the JavaScript library: https://github.com/pyscript/pyscript/issues/2100

    Source code in puepy/router.py
    def _micropython_parse_query_string(query_string):\n    \"\"\"\n    In MicroPython, urllib isn't available and we can't use the JavaScript library:\n    https://github.com/pyscript/pyscript/issues/2100\n    \"\"\"\n    if query_string and query_string[0] == \"?\":\n        query_string = query_string[1:]\n\n    def url_decode(s):\n        # Decode URL-encoded characters without using regex, which is also pretty broken in MicroPython...\n        i = 0\n        length = len(s)\n        decoded = []\n\n        while i < length:\n            if s[i] == \"%\":\n                if i + 2 < length:\n                    hex_value = s[i + 1 : i + 3]\n                    decoded.append(chr(int(hex_value, 16)))\n                    i += 3\n                else:\n                    decoded.append(\"%\")\n                    i += 1\n            elif s[i] == \"+\":\n                decoded.append(\" \")\n                i += 1\n            else:\n                decoded.append(s[i])\n                i += 1\n\n        return \"\".join(decoded)\n\n    params = {}\n    for part in query_string.split(\"&\"):\n        if \"=\" in part:\n            key, value = part.split(\"=\", 1)\n            key = url_decode(key)\n            value = url_decode(value)\n            if key in params:\n                params[key].append(value)\n            else:\n                params[key] = [value]\n        else:\n            key = url_decode(part)\n            if key in params:\n                params[key].append(\"\")\n            else:\n                params[key] = \"\"\n    return params\n
    "},{"location":"reference/storage/","title":"puepy.storage","text":""},{"location":"reference/storage/#puepystorage","title":"puepy.storage","text":"

    Browser Storage Module

    This module provides a BrowserStorage class that interfaces with browser storage objects such as localStorage and sessionStorage. It mimics dictionary-like behavior for interacting with storage items.

    Classes:

    Name Description BrowserStorage

    A class that provides dictionary-like access to browser storage objects.

    "},{"location":"reference/storage/#puepy.storage.BrowserStorage","title":"BrowserStorage","text":"

    Provides dictionary-like interface to browser storage objects.

    Attributes:

    Name Type Description target

    The browser storage object (e.g., localStorage, sessionStorage).

    description str

    Description of the storage instance.

    Source code in puepy/storage.py
    class BrowserStorage:\n    \"\"\"\n    Provides dictionary-like interface to browser storage objects.\n\n    Attributes:\n        target: The browser storage object (e.g., localStorage, sessionStorage).\n        description (str): Description of the storage instance.\n\n    \"\"\"\n\n    class NoDefault:\n        \"\"\"Placeholder class for default values when no default is provided.\"\"\"\n\n        pass\n\n    def __init__(self, target, description):\n        \"\"\"\n        Initializes the BrowserStorage instance.\n\n        Args:\n            target: The browser storage object.\n            description (str): Description of the storage instance.\n        \"\"\"\n        self.target = target\n        self.description = description\n\n    def __getitem__(self, key):\n        \"\"\"\n        Retrieves the value for a given key from the storage.\n\n        Args:\n            key (str): The key for the item to retrieve.\n\n        Returns:\n            The value associated with the key.\n\n        Raises:\n            KeyError: If the key does not exist in the storage.\n        \"\"\"\n        value = self.target.getItem(key)\n        if value is None:\n            raise KeyError(key)\n        return value\n\n    def __setitem__(self, key, value):\n        \"\"\"\n        Sets the value for a given key in the storage.\n\n        Args:\n            key (str): The key for the item to set.\n            value: The value to associate with the key.\n        \"\"\"\n        self.target.setItem(key, str(value))\n\n    def __delitem__(self, key):\n        \"\"\"\n        Deletes the item for a given key from the storage.\n\n        Args:\n            key (str): The key for the item to delete.\n\n        Raises:\n            KeyError: If the key does not exist in the storage.\n        \"\"\"\n        if self.target.getItem(key) is None:\n            raise KeyError(key)\n        self.target.removeItem(key)\n\n    def __contains__(self, key):\n        \"\"\"\n        Checks if a key exists in the storage.\n\n        Args:\n            key (str): The key to check.\n\n        Returns:\n            bool: True if the key exists, False otherwise.\n        \"\"\"\n        return not self.target.getItem(key) is None\n\n    def __len__(self):\n        \"\"\"\n        Returns the number of items in the storage.\n\n        Returns:\n            int: The number of items in the storage.\n        \"\"\"\n        return self.target.length\n\n    def __iter__(self):\n        \"\"\"\n        Returns an iterator over the keys in the storage.\n\n        Returns:\n            iterator: An iterator over the keys.\n        \"\"\"\n        return iter(self.keys())\n\n    def items(self):\n        \"\"\"\n        Returns an iterator over the (key, value) pairs in the storage.\n\n        Yields:\n            tuple: (key, value) pairs in the storage.\n        \"\"\"\n        for item in Object.entries(self.target):\n            yield item[0], item[1]\n\n    def keys(self):\n        \"\"\"\n        Returns a list of keys in the storage.\n\n        Returns:\n            list: A list of keys.\n        \"\"\"\n        return list(Object.keys(self.target))\n\n    def get(self, key, default=None):\n        \"\"\"\n        Retrieves the value for a given key, returning a default value if the key does not exist.\n\n        Args:\n            key (str): The key for the item to retrieve.\n            default: The default value to return if the key does not exist.\n\n        Returns:\n            The value associated with the key, or the default value.\n        \"\"\"\n        value = self.target.getItem(key)\n        if value is None:\n            return default\n        else:\n            return value\n\n    def clear(self):\n        \"\"\"\n        Clears all items from the storage.\n        \"\"\"\n        self.target.clear()\n\n    def copy(self):\n        \"\"\"\n        Returns a copy of the storage as a dictionary.\n\n        Returns:\n            dict: A dictionary containing all items in the storage.\n        \"\"\"\n        return dict(self.items())\n\n    def pop(self, key, default=NoDefault):\n        \"\"\"\n        Removes the item with the given key from the storage and returns its value.\n\n        Args:\n            key (str): The key for the item to remove.\n            default: The default value to return if the key does not exist.\n\n        Returns:\n            The value associated with the key, or the default value.\n\n        Raises:\n            KeyError: If the key does not exist and no default value is provided.\n        \"\"\"\n        value = self.target.getItem(key)\n        if value is None and default is self.NoDefault:\n            raise KeyError(key)\n        else:\n            self.target.removeItem(key)\n            return value\n\n    def popitem(self):\n        \"\"\"\n        Not implemented. Raises NotImplementedError.\n\n        Raises:\n            NotImplementedError: Always raised as the method is not implemented.\n        \"\"\"\n        raise NotImplementedError(\"popitem not implemented\")\n\n    def reversed(self):\n        \"\"\"\n        Not implemented. Raises NotImplementedError.\n\n        Raises:\n            NotImplementedError: Always raised as the method is not implemented.\n        \"\"\"\n        raise NotImplementedError(\"reversed not implemented\")\n\n    def setdefault(self, key, default=None):\n        \"\"\"\n        Sets the value for the key if it does not already exist in the storage.\n\n        Args:\n            key (str): The key for the item.\n            default: The value to set if the key does not exist.\n\n        Returns:\n            The value associated with the key, or the default value.\n        \"\"\"\n        if key in self:\n            return self[key]\n        else:\n            self[key] = default\n            return default\n\n    def update(self, other):\n        \"\"\"\n        Updates the storage with items from another dictionary or iterable of key-value pairs.\n\n        Args:\n            other: A dictionary or iterable of key-value pairs to update the storage with.\n        \"\"\"\n        for k, v in other.items():\n            self[k] = v\n\n    def values(self):\n        \"\"\"\n        Returns a list of values in the storage.\n\n        Returns:\n            list: A list of values.\n        \"\"\"\n        return list(Object.values(self.target))\n\n    def __str__(self):\n        return self.description\n\n    def __repr__(self):\n        return f\"<{self}>\"\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.NoDefault","title":"NoDefault","text":"

    Placeholder class for default values when no default is provided.

    Source code in puepy/storage.py
    class NoDefault:\n    \"\"\"Placeholder class for default values when no default is provided.\"\"\"\n\n    pass\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__contains__","title":"__contains__(key)","text":"

    Checks if a key exists in the storage.

    Parameters:

    Name Type Description Default key str

    The key to check.

    required

    Returns:

    Name Type Description bool

    True if the key exists, False otherwise.

    Source code in puepy/storage.py
    def __contains__(self, key):\n    \"\"\"\n    Checks if a key exists in the storage.\n\n    Args:\n        key (str): The key to check.\n\n    Returns:\n        bool: True if the key exists, False otherwise.\n    \"\"\"\n    return not self.target.getItem(key) is None\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__delitem__","title":"__delitem__(key)","text":"

    Deletes the item for a given key from the storage.

    Parameters:

    Name Type Description Default key str

    The key for the item to delete.

    required

    Raises:

    Type Description KeyError

    If the key does not exist in the storage.

    Source code in puepy/storage.py
    def __delitem__(self, key):\n    \"\"\"\n    Deletes the item for a given key from the storage.\n\n    Args:\n        key (str): The key for the item to delete.\n\n    Raises:\n        KeyError: If the key does not exist in the storage.\n    \"\"\"\n    if self.target.getItem(key) is None:\n        raise KeyError(key)\n    self.target.removeItem(key)\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__getitem__","title":"__getitem__(key)","text":"

    Retrieves the value for a given key from the storage.

    Parameters:

    Name Type Description Default key str

    The key for the item to retrieve.

    required

    Returns:

    Type Description

    The value associated with the key.

    Raises:

    Type Description KeyError

    If the key does not exist in the storage.

    Source code in puepy/storage.py
    def __getitem__(self, key):\n    \"\"\"\n    Retrieves the value for a given key from the storage.\n\n    Args:\n        key (str): The key for the item to retrieve.\n\n    Returns:\n        The value associated with the key.\n\n    Raises:\n        KeyError: If the key does not exist in the storage.\n    \"\"\"\n    value = self.target.getItem(key)\n    if value is None:\n        raise KeyError(key)\n    return value\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__init__","title":"__init__(target, description)","text":"

    Initializes the BrowserStorage instance.

    Parameters:

    Name Type Description Default target

    The browser storage object.

    required description str

    Description of the storage instance.

    required Source code in puepy/storage.py
    def __init__(self, target, description):\n    \"\"\"\n    Initializes the BrowserStorage instance.\n\n    Args:\n        target: The browser storage object.\n        description (str): Description of the storage instance.\n    \"\"\"\n    self.target = target\n    self.description = description\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__iter__","title":"__iter__()","text":"

    Returns an iterator over the keys in the storage.

    Returns:

    Name Type Description iterator

    An iterator over the keys.

    Source code in puepy/storage.py
    def __iter__(self):\n    \"\"\"\n    Returns an iterator over the keys in the storage.\n\n    Returns:\n        iterator: An iterator over the keys.\n    \"\"\"\n    return iter(self.keys())\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__len__","title":"__len__()","text":"

    Returns the number of items in the storage.

    Returns:

    Name Type Description int

    The number of items in the storage.

    Source code in puepy/storage.py
    def __len__(self):\n    \"\"\"\n    Returns the number of items in the storage.\n\n    Returns:\n        int: The number of items in the storage.\n    \"\"\"\n    return self.target.length\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.__setitem__","title":"__setitem__(key, value)","text":"

    Sets the value for a given key in the storage.

    Parameters:

    Name Type Description Default key str

    The key for the item to set.

    required value

    The value to associate with the key.

    required Source code in puepy/storage.py
    def __setitem__(self, key, value):\n    \"\"\"\n    Sets the value for a given key in the storage.\n\n    Args:\n        key (str): The key for the item to set.\n        value: The value to associate with the key.\n    \"\"\"\n    self.target.setItem(key, str(value))\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.clear","title":"clear()","text":"

    Clears all items from the storage.

    Source code in puepy/storage.py
    def clear(self):\n    \"\"\"\n    Clears all items from the storage.\n    \"\"\"\n    self.target.clear()\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.copy","title":"copy()","text":"

    Returns a copy of the storage as a dictionary.

    Returns:

    Name Type Description dict

    A dictionary containing all items in the storage.

    Source code in puepy/storage.py
    def copy(self):\n    \"\"\"\n    Returns a copy of the storage as a dictionary.\n\n    Returns:\n        dict: A dictionary containing all items in the storage.\n    \"\"\"\n    return dict(self.items())\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.get","title":"get(key, default=None)","text":"

    Retrieves the value for a given key, returning a default value if the key does not exist.

    Parameters:

    Name Type Description Default key str

    The key for the item to retrieve.

    required default

    The default value to return if the key does not exist.

    None

    Returns:

    Type Description

    The value associated with the key, or the default value.

    Source code in puepy/storage.py
    def get(self, key, default=None):\n    \"\"\"\n    Retrieves the value for a given key, returning a default value if the key does not exist.\n\n    Args:\n        key (str): The key for the item to retrieve.\n        default: The default value to return if the key does not exist.\n\n    Returns:\n        The value associated with the key, or the default value.\n    \"\"\"\n    value = self.target.getItem(key)\n    if value is None:\n        return default\n    else:\n        return value\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.items","title":"items()","text":"

    Returns an iterator over the (key, value) pairs in the storage.

    Yields:

    Name Type Description tuple

    (key, value) pairs in the storage.

    Source code in puepy/storage.py
    def items(self):\n    \"\"\"\n    Returns an iterator over the (key, value) pairs in the storage.\n\n    Yields:\n        tuple: (key, value) pairs in the storage.\n    \"\"\"\n    for item in Object.entries(self.target):\n        yield item[0], item[1]\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.keys","title":"keys()","text":"

    Returns a list of keys in the storage.

    Returns:

    Name Type Description list

    A list of keys.

    Source code in puepy/storage.py
    def keys(self):\n    \"\"\"\n    Returns a list of keys in the storage.\n\n    Returns:\n        list: A list of keys.\n    \"\"\"\n    return list(Object.keys(self.target))\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.pop","title":"pop(key, default=NoDefault)","text":"

    Removes the item with the given key from the storage and returns its value.

    Parameters:

    Name Type Description Default key str

    The key for the item to remove.

    required default

    The default value to return if the key does not exist.

    NoDefault

    Returns:

    Type Description

    The value associated with the key, or the default value.

    Raises:

    Type Description KeyError

    If the key does not exist and no default value is provided.

    Source code in puepy/storage.py
    def pop(self, key, default=NoDefault):\n    \"\"\"\n    Removes the item with the given key from the storage and returns its value.\n\n    Args:\n        key (str): The key for the item to remove.\n        default: The default value to return if the key does not exist.\n\n    Returns:\n        The value associated with the key, or the default value.\n\n    Raises:\n        KeyError: If the key does not exist and no default value is provided.\n    \"\"\"\n    value = self.target.getItem(key)\n    if value is None and default is self.NoDefault:\n        raise KeyError(key)\n    else:\n        self.target.removeItem(key)\n        return value\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.popitem","title":"popitem()","text":"

    Not implemented. Raises NotImplementedError.

    Raises:

    Type Description NotImplementedError

    Always raised as the method is not implemented.

    Source code in puepy/storage.py
    def popitem(self):\n    \"\"\"\n    Not implemented. Raises NotImplementedError.\n\n    Raises:\n        NotImplementedError: Always raised as the method is not implemented.\n    \"\"\"\n    raise NotImplementedError(\"popitem not implemented\")\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.reversed","title":"reversed()","text":"

    Not implemented. Raises NotImplementedError.

    Raises:

    Type Description NotImplementedError

    Always raised as the method is not implemented.

    Source code in puepy/storage.py
    def reversed(self):\n    \"\"\"\n    Not implemented. Raises NotImplementedError.\n\n    Raises:\n        NotImplementedError: Always raised as the method is not implemented.\n    \"\"\"\n    raise NotImplementedError(\"reversed not implemented\")\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.setdefault","title":"setdefault(key, default=None)","text":"

    Sets the value for the key if it does not already exist in the storage.

    Parameters:

    Name Type Description Default key str

    The key for the item.

    required default

    The value to set if the key does not exist.

    None

    Returns:

    Type Description

    The value associated with the key, or the default value.

    Source code in puepy/storage.py
    def setdefault(self, key, default=None):\n    \"\"\"\n    Sets the value for the key if it does not already exist in the storage.\n\n    Args:\n        key (str): The key for the item.\n        default: The value to set if the key does not exist.\n\n    Returns:\n        The value associated with the key, or the default value.\n    \"\"\"\n    if key in self:\n        return self[key]\n    else:\n        self[key] = default\n        return default\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.update","title":"update(other)","text":"

    Updates the storage with items from another dictionary or iterable of key-value pairs.

    Parameters:

    Name Type Description Default other

    A dictionary or iterable of key-value pairs to update the storage with.

    required Source code in puepy/storage.py
    def update(self, other):\n    \"\"\"\n    Updates the storage with items from another dictionary or iterable of key-value pairs.\n\n    Args:\n        other: A dictionary or iterable of key-value pairs to update the storage with.\n    \"\"\"\n    for k, v in other.items():\n        self[k] = v\n
    "},{"location":"reference/storage/#puepy.storage.BrowserStorage.values","title":"values()","text":"

    Returns a list of values in the storage.

    Returns:

    Name Type Description list

    A list of values.

    Source code in puepy/storage.py
    def values(self):\n    \"\"\"\n    Returns a list of values in the storage.\n\n    Returns:\n        list: A list of values.\n    \"\"\"\n    return list(Object.values(self.target))\n
    "},{"location":"reference/tag/","title":"puepy.core.Tag","text":"

    Tags should not be created directly

    In your populate() method, call t.tag_name() to create a tag. There's no reason an application develop should directly instanciate a tag instance and doing so is not supported.

    The most basic building block of a PuePy app. A Tag is a single HTML element. This is also the base class of Component, which is then the base class of Page.

    Attributes:

    Name Type Description default_classes list

    Default classes for the tag.

    default_attrs dict

    Default attributes for the tag.

    default_role str

    Default role for the tag.

    page Page

    The page the tag is on.

    router Router or None

    The router the application is using, if any.

    parent Tag

    The parent tag, component, or page.

    application Application

    The application instance.

    element

    The rendered element on the DOM. Raises ElementNotInDom if not found.

    children list

    The children of the tag.

    refs dict

    The refs of the tag.

    tag_name str

    The name of the tag.

    ref str

    The reference of the tag.

    Source code in puepy/core.py
    class Tag:\n    \"\"\"\n    The most basic building block of a PuePy app. A Tag is a single HTML element. This is also the base class of\n    `Component`, which is then the base class of `Page`.\n\n    Attributes:\n        default_classes (list): Default classes for the tag.\n        default_attrs (dict): Default attributes for the tag.\n        default_role (str): Default role for the tag.\n        page (Page): The page the tag is on.\n        router (Router or None): The router the application is using, if any.\n        parent (Tag): The parent tag, component, or page.\n        application (Application): The application instance.\n        element: The rendered element on the DOM. Raises ElementNotInDom if not found.\n        children (list): The children of the tag.\n        refs (dict): The refs of the tag.\n        tag_name (str): The name of the tag.\n        ref (str): The reference of the tag.\n    \"\"\"\n\n    stack = []\n    population_stack = []\n    origin_stack = [[]]\n    component_stack = []\n    default_classes = []\n    default_attrs = {}\n    default_role = None\n\n    document = document\n\n    # noinspection t\n    def __init__(\n        self,\n        tag_name,\n        ref,\n        page: \"Page\" = None,\n        parent=None,\n        parent_component=None,\n        origin=None,\n        children=None,\n        **kwargs,\n    ):\n        # Kept so we can garbage collect them later\n        self._added_event_listeners = []\n\n        # Ones manually added, which we persist when reconfigured\n        self._manually_added_event_listeners = {}\n\n        # The rendered element\n        self._rendered_element = None\n\n        # Child nodes and origin refs\n        self.children = []\n        self.refs = {}\n\n        self.tag_name = tag_name\n        self.ref = ref\n\n        # Attrs that webcomponents create that we need to preserve\n        self._retained_attrs = {}\n\n        # Add any children passed to constructor\n        if children:\n            self.add(*children)\n\n        # Configure self._page\n        if isinstance(page, Page):\n            self._page = page\n        elif isinstance(self, Page):\n            self._page = self\n        elif page:\n            raise Exception(f\"Unknown page type {type(page)}\")\n        else:\n            raise Exception(\"No page passed\")\n\n        if \"id\" in kwargs:\n            self._element_id = kwargs[\"id\"]\n        elif self._page and self._page.application:\n            self._element_id = self._page.application.element_id_generator.get_id_for_element(self)\n        else:\n            self._element_id = f\"ppauto-{id(self)}\"\n\n        if isinstance(parent, Tag):\n            self.parent = parent\n            parent.add(self)\n        elif parent:\n            raise Exception(f\"Unknown parent type {type(parent)}: {repr(parent)}\")\n        else:\n            self.parent = None\n\n        if isinstance(parent_component, Component):\n            self.parent_component = parent_component\n        elif parent_component:\n            raise Exception(f\"Unknown parent_component type {type(parent_component)}: {repr(parent_component)}\")\n        else:\n            self.parent_component = None\n\n        self.origin = origin\n        self._children_generated = False\n\n        self._configure(kwargs)\n\n    def __del__(self):\n        if not is_server_side:\n            while self._added_event_listeners:\n                remove_event_listener(*self._added_event_listeners.pop())\n\n    @property\n    def application(self):\n        return self._page._application\n\n    def _configure(self, kwargs):\n        self._kwarg_event_listeners = _extract_event_handlers(kwargs)\n        self._handle_bind(kwargs)\n        self._handle_attrs(kwargs)\n\n    def _handle_bind(self, kwargs):\n        if \"bind\" in kwargs:\n            self.bind = kwargs.pop(\"bind\")\n            input_type = kwargs.get(\"type\")\n            tag_name = self.tag_name.lower()\n\n            if \"value\" in kwargs and not (tag_name == \"input\" and input_type == \"radio\"):\n                raise Exception(\"Cannot specify both 'bind' and 'value'\")\n\n        else:\n            self.bind = None\n\n    def _handle_attrs(self, kwargs):\n        self.attrs = self._retained_attrs.copy()\n        for k, v in kwargs.items():\n            if hasattr(self, f\"set_{k}\"):\n                getattr(self, f\"set_{k}\")(v)\n            else:\n                self.attrs[k] = v\n\n    def populate(self):\n        \"\"\"To be overwritten by subclasses, this method will define the composition of the element\"\"\"\n        pass\n\n    def precheck(self):\n        \"\"\"\n        Before doing too much work, decide whether rendering this tag should raise an error or not. This may be useful,\n        especially on a Page, to check if the user is authorized to view the page, for example:\n\n        Examples:\n            ``` py\n            def precheck(self):\n                if not self.application.state[\"authenticated_user\"]:\n                    raise exceptions.Unauthorized()\n            ```\n        \"\"\"\n        pass\n\n    def generate_children(self):\n        \"\"\"\n        Runs populate, but first adds self to self.population_stack, and removes it after populate runs.\n\n        That way, as populate is executed, self.population_stack can be used to figure out what the innermost populate()\n        method is being run and thus, where to send bind= parameters.\n        \"\"\"\n        self.origin_stack.append([])\n        self._refs_pending_removal = self.refs.copy()\n        self.refs = {}\n        self.population_stack.append(self)\n        try:\n            self.precheck()\n            self.populate()\n        finally:\n            self.population_stack.pop()\n            self.origin_stack.pop()\n\n    def render(self):\n        attrs = self.get_default_attrs()\n        attrs.update(self.attrs)\n\n        element = self._create_element(attrs)\n\n        self._render_onto(element, attrs)\n        self.post_render(element)\n        return element\n\n    def _create_element(self, attrs):\n        if \"xmlns\" in attrs:\n            element = self.document.createElementNS(attrs.get(\"xmlns\"), self.tag_name)\n        else:\n            element = self.document.createElement(self.tag_name)\n\n        element.setAttribute(\"id\", self.element_id)\n        if is_server_side:\n            element.setIdAttribute(\"id\")\n\n        self.configure_element(element)\n\n        return element\n\n    def configure_element(self, element):\n        pass\n\n    def post_render(self, element):\n        pass\n\n    @property\n    def element_id(self):\n        return self._element_id\n\n    @property\n    def element(self):\n        el = self.document.getElementById(self.element_id)\n        if el:\n            return el\n        else:\n            raise ElementNotInDom(self.element_id)\n\n    # noinspection t\n    def _render_onto(self, element, attrs):\n        self._rendered_element = element\n\n        # Handle classes\n        classes = self.get_render_classes(attrs)\n\n        if classes:\n            # element.className = \" \".join(classes)\n            element.setAttribute(\"class\", \" \".join(classes))\n\n        # Add attributes\n        for key, value in attrs.items():\n            if key not in (\"class_name\", \"classes\", \"class\"):\n                if hasattr(self, f\"handle_{key}_attr\"):\n                    getattr(self, f\"handle_{key}_attr\")(element, value)\n                else:\n                    if key.endswith(\"_\"):\n                        attr = key[:-1]\n                    else:\n                        attr = key\n                    attr = attr.replace(\"_\", \"-\")\n\n                    if isinstance(value, bool) or value is None:\n                        if value:\n                            element.setAttribute(attr, attr)\n                    elif isinstance(value, (str, int, float)):\n                        element.setAttribute(attr, value)\n                    else:\n                        element.setAttribute(attr, str(value))\n\n        if \"role\" not in attrs and self.default_role:\n            element.setAttribute(\"role\", self.default_role)\n\n        # Add event handlers\n        self._add_listeners(element, self._kwarg_event_listeners)\n        self._add_listeners(element, self._manually_added_event_listeners)\n\n        # Add bind\n        if self.bind and self.origin:\n            input_type = _element_input_type(element)\n\n            if type(self.bind) in [list, tuple]:\n                value = self.origin.state\n                for key in self.bind:\n                    value = value[key]\n            else:\n                value = self.origin.state[self.bind]\n\n            if input_type == \"checkbox\":\n                if is_server_side and value:\n                    element.setAttribute(\"checked\", value)\n                else:\n                    element.checked = bool(value)\n                    element.setAttribute(\"checked\", value)\n                event_type = \"change\"\n            elif input_type == \"radio\":\n                is_checked = value == element.value\n                if is_server_side and is_checked:\n                    element.setAttribute(\"checked\", is_checked)\n                else:\n                    element.checked = is_checked\n                    element.setAttribute(\"checked\", is_checked)\n                event_type = \"change\"\n            else:\n                if is_server_side:\n                    element.setAttribute(\"value\", value)\n                else:\n                    element.value = value\n                    element.setAttribute(\"value\", value)\n                event_type = \"input\"\n            self.add_event_listener(element, event_type, self.on_bind_input)\n        elif self.bind:\n            raise Exception(\"Cannot specify bind a valid parent component\")\n\n        self.render_children(element)\n\n    def _add_listeners(self, element, listeners):\n        for key, value in listeners.items():\n            key = key.replace(\"_\", \"-\")\n            if isinstance(value, (list, tuple)):\n                for handler in value:\n                    self.add_event_listener(element, key, handler)\n            else:\n                self.add_event_listener(element, key, value)\n\n    def render_children(self, element):\n        for child in self.children:\n            if isinstance(child, Slot):\n                if child.children:  # If slots don't have any children, don't bother.\n                    element.appendChild(child.render())\n            elif isinstance(child, Tag):\n                element.appendChild(child.render())\n            elif isinstance(child, html):\n                element.insertAdjacentHTML(\"beforeend\", str(child))\n            elif isinstance(child, str):\n                element.appendChild(self.document.createTextNode(child))\n            elif child is None:\n                pass\n            elif getattr(child, \"nodeType\", None) is not None:\n                # DOM element\n                element.appendChild(child)\n            else:\n                self.render_unknown_child(element, child)\n\n    def render_unknown_child(self, element, child):\n        \"\"\"\n        Called when the child is not a Tag, Slot, or html. By default, it raises an error.\n        \"\"\"\n        raise Exception(f\"Unknown child type {type(child)} onto {self}\")\n\n    def get_render_classes(self, attrs):\n        class_names, python_css_classes = merge_classes(\n            set(self.get_default_classes()),\n            attrs.pop(\"class_name\", []),\n            attrs.pop(\"classes\", []),\n            attrs.pop(\"class\", []),\n        )\n        self.page.python_css_classes.update(python_css_classes)\n        return class_names\n\n    def get_default_classes(self):\n        \"\"\"\n        Returns a shallow copy of the default_classes list.\n\n        This could be overridden by subclasses to provide a different default_classes list.\n\n        Returns:\n            (list): A shallow copy of the default_classes list.\n        \"\"\"\n        return self.default_classes.copy()\n\n    def get_default_attrs(self):\n        return self.default_attrs.copy()\n\n    def add_event_listener(self, element, event, listener):\n        \"\"\"\n        Just an internal wrapper around add_event_listener (JS function) that keeps track of what we added, so\n        we can garbage collect it later.\n\n        Should probably not be used outside this class.\n        \"\"\"\n        self._added_event_listeners.append((element, event, listener))\n        if not is_server_side:\n            add_event_listener(element, event, listener)\n\n    def mount(self, selector_or_element):\n        self.update_title()\n        if not self._children_generated:\n            with self:\n                self.generate_children()\n\n        if isinstance(selector_or_element, str):\n            element = self.document.querySelector(selector_or_element)\n        else:\n            element = selector_or_element\n\n        if not element:\n            raise RuntimeError(f\"Element {selector_or_element} not found\")\n\n        element.innerHTML = \"\"\n        element.appendChild(self.render())\n        self.recursive_call(\"on_ready\")\n        self.add_python_css_classes()\n\n    def add_python_css_classes(self):\n        \"\"\"\n        This is only done at the page level.\n        \"\"\"\n        pass\n\n    def recursive_call(self, method, *args, **kwargs):\n        \"\"\"\n        Recursively call a specified method on all child Tag objects.\n\n        Args:\n            method (str): The name of the method to be called on each Tag object.\n            *args: Optional arguments to be passed to the method.\n            **kwargs: Optional keyword arguments to be passed to the method.\n        \"\"\"\n        for child in self.children:\n            if isinstance(child, Tag):\n                child.recursive_call(method, *args, **kwargs)\n        getattr(self, method)(*args, **kwargs)\n\n    def on_ready(self):\n        pass\n\n    def _retain_implicit_attrs(self):\n        \"\"\"\n        Retain attributes set elsewhere\n        \"\"\"\n        try:\n            for attr in self.element.attributes:\n                if attr.name not in self.attrs and attr.name != \"id\":\n                    self._retained_attrs[attr.name] = attr.value\n        except ElementNotInDom:\n            pass\n\n    def on_redraw(self):\n        pass\n\n    def on_bind_input(self, event):\n        input_type = _element_input_type(event.target)\n        if input_type == \"checkbox\":\n            self.set_bind_value(self.bind, event.target.checked)\n        elif input_type == \"radio\":\n            if event.target.checked:\n                self.set_bind_value(self.bind, event.target.value)\n        elif input_type == \"number\":\n            value = event.target.value\n            try:\n                if \".\" in str(value):\n                    value = float(value)\n                else:\n                    value = int(value)\n            except (ValueError, TypeError):\n                pass\n            self.set_bind_value(self.bind, value)\n        else:\n            self.set_bind_value(self.bind, event.target.value)\n\n    def set_bind_value(self, bind, value):\n        if type(bind) in (list, tuple):\n            nested_dict = self.origin.state\n            for key in bind[:-1]:\n                nested_dict = nested_dict[key]\n            with self.origin.state.mutate(bind[0]):\n                nested_dict[bind[-1]] = value\n        else:\n            self.origin.state[self.bind] = value\n\n    @property\n    def page(self):\n        if self._page:\n            return self._page\n        elif isinstance(self, Page):\n            return self\n\n    @property\n    def router(self):\n        if self.application:\n            return self.application.router\n\n    @property\n    def parent(self):\n        return self._parent\n\n    @parent.setter\n    def parent(self, new_parent):\n        existing_parent = getattr(self, \"_parent\", None)\n        if new_parent == existing_parent:\n            if new_parent and self not in new_parent.children:\n                existing_parent.children.append(self)\n            return\n\n        if existing_parent and self in existing_parent.children:\n            existing_parent.children.remove(self)\n        if new_parent and self not in new_parent.children:\n            new_parent.children.append(self)\n\n        self._parent = new_parent\n\n    def add(self, *children):\n        for child in children:\n            if isinstance(child, Tag):\n                child.parent = self\n            else:\n                self.children.append(child)\n\n    def redraw(self):\n        if self in self.page.redraw_list:\n            self.page.redraw_list.remove(self)\n\n        try:\n            element = self.element\n        except ElementNotInDom:\n            return\n\n        if is_server_side:\n            old_active_element_id = None\n        else:\n            old_active_element_id = self.document.activeElement.id if self.document.activeElement else None\n\n            self.recursive_call(\"_retain_implicit_attrs\")\n\n        self.children = []\n\n        attrs = self.get_default_attrs()\n        attrs.update(self.attrs)\n\n        self.update_title()\n        with self:\n            self.generate_children()\n\n        staging_element = self._create_element(attrs)\n\n        self._render_onto(staging_element, attrs)\n\n        patch_dom_element(staging_element, element)\n\n        if old_active_element_id is not None:\n            el = self.document.getElementById(old_active_element_id)\n            if el:\n                el.focus()\n\n        self.recursive_call(\"on_redraw\")\n\n    def trigger_event(self, event, detail=None, **kwargs):\n        \"\"\"\n                Triggers an event to be consumed by code using this class.\n\n                Args:\n                    event (str): The name of the event to trigger. If the event name contains underscores, a warning message is printed suggesting to use dashes instead.\n                    detail (dict, optional): Additional data to be sent with the event. This should be a dictionary where the keys and values will be converted to JavaScript objects.\n                    **kwargs: Additional keyword arguments. These arguments are not used in the implementation of the method and are ignored.\n        \u00df\"\"\"\n        if \"_\" in event:\n            print(\"Triggering event with underscores. Did you mean dashes?: \", event)\n\n        # noinspection PyUnresolvedReferences\n        from pyscript.ffi import to_js\n\n        # noinspection PyUnresolvedReferences\n        from js import Object, Map\n\n        if detail:\n            event_object = to_js({\"detail\": Map.new(Object.entries(to_js(detail)))})\n        else:\n            event_object = to_js({})\n\n        self.element.dispatchEvent(CustomEvent.new(event, event_object))\n\n    def update_title(self):\n        \"\"\"\n        To be overridden by subclasses (usually pages), this method should update the Window title as needed.\n\n        Called on mounting or redraw.\n        \"\"\"\n        pass\n\n    def __enter__(self):\n        self.stack.append(self)\n        self.origin_stack[0].append(self)\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.stack.pop()\n        self.origin_stack[0].pop()\n        return False\n\n    def __str__(self):\n        return self.tag_name\n\n    def __repr__(self):\n        return f\"<{self} ({id(self)})>\"\n
    "},{"location":"reference/tag/#puepy.core.Tag._retain_implicit_attrs","title":"_retain_implicit_attrs()","text":"

    Retain attributes set elsewhere

    Source code in puepy/core.py
    def _retain_implicit_attrs(self):\n    \"\"\"\n    Retain attributes set elsewhere\n    \"\"\"\n    try:\n        for attr in self.element.attributes:\n            if attr.name not in self.attrs and attr.name != \"id\":\n                self._retained_attrs[attr.name] = attr.value\n    except ElementNotInDom:\n        pass\n
    "},{"location":"reference/tag/#puepy.core.Tag.add_event_listener","title":"add_event_listener(element, event, listener)","text":"

    Just an internal wrapper around add_event_listener (JS function) that keeps track of what we added, so we can garbage collect it later.

    Should probably not be used outside this class.

    Source code in puepy/core.py
    def add_event_listener(self, element, event, listener):\n    \"\"\"\n    Just an internal wrapper around add_event_listener (JS function) that keeps track of what we added, so\n    we can garbage collect it later.\n\n    Should probably not be used outside this class.\n    \"\"\"\n    self._added_event_listeners.append((element, event, listener))\n    if not is_server_side:\n        add_event_listener(element, event, listener)\n
    "},{"location":"reference/tag/#puepy.core.Tag.add_python_css_classes","title":"add_python_css_classes()","text":"

    This is only done at the page level.

    Source code in puepy/core.py
    def add_python_css_classes(self):\n    \"\"\"\n    This is only done at the page level.\n    \"\"\"\n    pass\n
    "},{"location":"reference/tag/#puepy.core.Tag.generate_children","title":"generate_children()","text":"

    Runs populate, but first adds self to self.population_stack, and removes it after populate runs.

    That way, as populate is executed, self.population_stack can be used to figure out what the innermost populate() method is being run and thus, where to send bind= parameters.

    Source code in puepy/core.py
    def generate_children(self):\n    \"\"\"\n    Runs populate, but first adds self to self.population_stack, and removes it after populate runs.\n\n    That way, as populate is executed, self.population_stack can be used to figure out what the innermost populate()\n    method is being run and thus, where to send bind= parameters.\n    \"\"\"\n    self.origin_stack.append([])\n    self._refs_pending_removal = self.refs.copy()\n    self.refs = {}\n    self.population_stack.append(self)\n    try:\n        self.precheck()\n        self.populate()\n    finally:\n        self.population_stack.pop()\n        self.origin_stack.pop()\n
    "},{"location":"reference/tag/#puepy.core.Tag.get_default_classes","title":"get_default_classes()","text":"

    Returns a shallow copy of the default_classes list.

    This could be overridden by subclasses to provide a different default_classes list.

    Returns:

    Type Description list

    A shallow copy of the default_classes list.

    Source code in puepy/core.py
    def get_default_classes(self):\n    \"\"\"\n    Returns a shallow copy of the default_classes list.\n\n    This could be overridden by subclasses to provide a different default_classes list.\n\n    Returns:\n        (list): A shallow copy of the default_classes list.\n    \"\"\"\n    return self.default_classes.copy()\n
    "},{"location":"reference/tag/#puepy.core.Tag.populate","title":"populate()","text":"

    To be overwritten by subclasses, this method will define the composition of the element

    Source code in puepy/core.py
    def populate(self):\n    \"\"\"To be overwritten by subclasses, this method will define the composition of the element\"\"\"\n    pass\n
    "},{"location":"reference/tag/#puepy.core.Tag.precheck","title":"precheck()","text":"

    Before doing too much work, decide whether rendering this tag should raise an error or not. This may be useful, especially on a Page, to check if the user is authorized to view the page, for example:

    Examples:

    def precheck(self):\n    if not self.application.state[\"authenticated_user\"]:\n        raise exceptions.Unauthorized()\n
    Source code in puepy/core.py
    def precheck(self):\n    \"\"\"\n    Before doing too much work, decide whether rendering this tag should raise an error or not. This may be useful,\n    especially on a Page, to check if the user is authorized to view the page, for example:\n\n    Examples:\n        ``` py\n        def precheck(self):\n            if not self.application.state[\"authenticated_user\"]:\n                raise exceptions.Unauthorized()\n        ```\n    \"\"\"\n    pass\n
    "},{"location":"reference/tag/#puepy.core.Tag.recursive_call","title":"recursive_call(method, *args, **kwargs)","text":"

    Recursively call a specified method on all child Tag objects.

    Parameters:

    Name Type Description Default method str

    The name of the method to be called on each Tag object.

    required *args

    Optional arguments to be passed to the method.

    () **kwargs

    Optional keyword arguments to be passed to the method.

    {} Source code in puepy/core.py
    def recursive_call(self, method, *args, **kwargs):\n    \"\"\"\n    Recursively call a specified method on all child Tag objects.\n\n    Args:\n        method (str): The name of the method to be called on each Tag object.\n        *args: Optional arguments to be passed to the method.\n        **kwargs: Optional keyword arguments to be passed to the method.\n    \"\"\"\n    for child in self.children:\n        if isinstance(child, Tag):\n            child.recursive_call(method, *args, **kwargs)\n    getattr(self, method)(*args, **kwargs)\n
    "},{"location":"reference/tag/#puepy.core.Tag.render_unknown_child","title":"render_unknown_child(element, child)","text":"

    Called when the child is not a Tag, Slot, or html. By default, it raises an error.

    Source code in puepy/core.py
    def render_unknown_child(self, element, child):\n    \"\"\"\n    Called when the child is not a Tag, Slot, or html. By default, it raises an error.\n    \"\"\"\n    raise Exception(f\"Unknown child type {type(child)} onto {self}\")\n
    "},{"location":"reference/tag/#puepy.core.Tag.trigger_event","title":"trigger_event(event, detail=None, **kwargs)","text":"
        Triggers an event to be consumed by code using this class.\n\n    Args:\n        event (str): The name of the event to trigger. If the event name contains underscores, a warning message is printed suggesting to use dashes instead.\n        detail (dict, optional): Additional data to be sent with the event. This should be a dictionary where the keys and values will be converted to JavaScript objects.\n        **kwargs: Additional keyword arguments. These arguments are not used in the implementation of the method and are ignored.\n

    \u00df

    Source code in puepy/core.py
    def trigger_event(self, event, detail=None, **kwargs):\n    \"\"\"\n            Triggers an event to be consumed by code using this class.\n\n            Args:\n                event (str): The name of the event to trigger. If the event name contains underscores, a warning message is printed suggesting to use dashes instead.\n                detail (dict, optional): Additional data to be sent with the event. This should be a dictionary where the keys and values will be converted to JavaScript objects.\n                **kwargs: Additional keyword arguments. These arguments are not used in the implementation of the method and are ignored.\n    \u00df\"\"\"\n    if \"_\" in event:\n        print(\"Triggering event with underscores. Did you mean dashes?: \", event)\n\n    # noinspection PyUnresolvedReferences\n    from pyscript.ffi import to_js\n\n    # noinspection PyUnresolvedReferences\n    from js import Object, Map\n\n    if detail:\n        event_object = to_js({\"detail\": Map.new(Object.entries(to_js(detail)))})\n    else:\n        event_object = to_js({})\n\n    self.element.dispatchEvent(CustomEvent.new(event, event_object))\n
    "},{"location":"reference/tag/#puepy.core.Tag.update_title","title":"update_title()","text":"

    To be overridden by subclasses (usually pages), this method should update the Window title as needed.

    Called on mounting or redraw.

    Source code in puepy/core.py
    def update_title(self):\n    \"\"\"\n    To be overridden by subclasses (usually pages), this method should update the Window title as needed.\n\n    Called on mounting or redraw.\n    \"\"\"\n    pass\n
    "},{"location":"tutorial/00-using-this-tutorial/","title":"Using This Tutorial","text":"

    Each of the examples in this tutorial can be run on PyScript.com, a web environment that lets you write, test, and share PyScript code. Alternatively, you can clone the PuePy git repo and run a live web server with each example included.

    The PyScript.com environment uses the PuePy .whl file, as downloadable from PyPi, while the examples in the git repository are served with PuePy directly from source files on disk.

    "},{"location":"tutorial/00-using-this-tutorial/#using-pyscriptcom","title":"Using PyScript.com","text":"

    Navigate to https://pyscript.com/@kkinder/puepy-tutorial/latest and you are greeted with a list of files on the left, a code editor in the middle, and a running example on the left. Each chapter in the tutorial corresponds with a directory in tutorial folder on the left.

    You can clone the entire examples project and edit it yourself to continue your learning:

    Once cloned you make your own changes and experiment with them in real time.

    "},{"location":"tutorial/00-using-this-tutorial/#editing-locally","title":"Editing locally","text":"

    After cloning puepy on git, you can run the examples using a simple script:

    http://localhost:8000/ show you a list of examples

    As you edit them in the examples folder and reload the window, your changes will be live.

    "},{"location":"tutorial/00-using-this-tutorial/#live-examples","title":"Live Examples","text":"

    Most of the examples you see live in this tutorial include example code running live in a browser like this:

    There, you can see the running example inline with its explanation. You can also edit the code on PyScript.com by navigating to its example folder and cloning the project, as described above.

    "},{"location":"tutorial/01-hello-world/","title":"Hello, World! 0.6.2","text":"

    0.6.2 Let's start with the simplest possible: Hello, World!

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/01_hello_world/index.htmlEdit

    hello_world.pyindex.htmlpyscript.json
    from puepy import Application, Page, t\n\napp = Application()\n\n\n@app.page()\nclass HelloWorldPage(Page):\n    def populate(self):\n        t.h1(\"Hello, World!\")\n\n\napp.mount(\"#app\")\n
    <!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <title>PuePy Hello, World</title>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n  <link rel=\"stylesheet\" href=\"https://pyscript.net/releases/2025.2.2/core.css\">\n  <script type=\"module\" src=\"https://pyscript.net/releases/2025.2.2/core.js\"></script>\n</head>\n<body>\n<div id=\"app\">Loading...</div>\n<script type=\"mpy\" src=\"./hello_world.py\" config=\"../../pyscript.json\"></script>\n</body>\n</html>\n
    {\n  \"name\": \"PuePy Tutorial\",\n  \"debug\": true,\n  \"files\": {},\n  \"js_modules\": {\n    \"main\": {\n      \"https://cdn.jsdelivr.net/npm/morphdom@2.7.4/+esm\": \"morphdom\"\n    }\n  },\n  \"packages\": [\n    \"./puepy-0.6.2-py3-none-any.whl\"\n  ]\n}\n
    "},{"location":"tutorial/01-hello-world/#including-pyscript","title":"Including PyScript","text":"

    Let's start with the HTML. To use PuePy, we include PyScript from its CDN:

    index.html
      <link rel=\"stylesheet\" href=\"https://pyscript.net/releases/2025.2.2/core.css\">\n  <script type=\"module\" src=\"https://pyscript.net/releases/2025.2.2/core.js\"></script>\n

    Then, we include our PyScript config file and also execute our hello_world.py file:

    index.html
    <script type=\"mpy\" src=\"./hello_world.py\" config=\"../../pyscript.json\"></script>\n
    "},{"location":"tutorial/01-hello-world/#pyscript-configuration","title":"PyScript configuration","text":"

    PyScript Configuration

    The official PyScript documentation has more information on PyScript configuration.

    The PyScript configuration must, at minimum, tell PyScript to use PuePy (usually as a package) and include Morphdom, which is a dependency of PuePy.

    "},{"location":"tutorial/01-hello-world/#the-python-code","title":"The Python Code","text":"

    Let's take a look at our Python code which actually renders Hello, World.

    First, we import Application, Page, and t from puepy:

    from puepy import Application, Page, t\n

    To use PuePy, you must always create an Application instance, even if the application only has one page:

    app = Application()\n

    Next, we define a Page and use the t singleton to compose our DOM in the populate() method. Don't worry too much about the details for now; just know that this is how we define pages and components in PuePy:

    @app.page()\nclass HelloWorldPage(Page):\n    def populate(self):\n        t.h1(\"Hello, World!\")\n

    Finally, we tell PuePy where to mount the application. This is where the application will be rendered in the DOM. The #app element was already defined in our HTML file.

    app.mount(\"#app\")\n

    And with that, the page is added to the application, and the application is mounted in the element with id app.

    Watching for Errors

    Use your browser's development console to watch for any errors.

    "},{"location":"tutorial/02-hello-name/","title":"Hello, Name","text":"

    In this chapter, we introduce state and variables by creating a simple form that asks for a name and greets the user.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/02_hello_name/index.htmlEdit

    The html and pyscript configuration are the same as in the previous Hello, World chapter, so we will only study the Python code. Expand the annotations in the code below for a more detail explanation of the changes:

    hello_name.py
    from puepy import Application, Page, t\n\napp = Application()\n\n\n@app.page()\nclass HelloNamePage(Page):\n    def initial(self):\n        return {\"name\": \"\"}  # (1)\n\n    def populate(self):\n        if self.state[\"name\"]:  # (2)\n            t.h1(f\"Hello, {self.state['name']}!\")\n        else:\n            t.h1(f\"Why don't you tell me your name?\")\n\n        with t.div(style=\"margin: 1em\"):\n            t.input(bind=\"name\", placeholder=\"name\", autocomplete=\"off\")  # (3)\n\n\napp.mount(\"#app\")\n
    1. The initial() method defines the page's initial working state. In this case, it returns a dictionary with a single key, name, which is initially an empty string.
    2. We check the value of self.state[\"name\"] and renders different content based on that value.
    3. We define an input element with a bind=\"name\" parameter. This binds the input element to the name key in the page's state. When the input value changes, the state is updated, and the page is re-rendered.
    "},{"location":"tutorial/02-hello-name/#reactivity","title":"Reactivity","text":"

    A page or component's initial state is defined by the initial() method. If implemented, it should return a dictionary, which is then stored as a special reactive dictionary, self.state. As the state is modified, the component redraws, updating the DOM as needed.

    Modifying .state values in-place will not work

    For complex objects like lists and dictionaries, you cannot modify them in-place and expect the component to re-render.

    # THESE WILL NOT WORK:\nself.state[\"my_list\"].append(\"spam\")\nself.state[\"my_dict\"][\"spam\"] = \"eggs\"\n

    This is because PuePy's ReactiveDict cannot detect \"deep\" changes to state automatically. If you are modifying objects in-place, use with self.state.mutate() as a context manager:

    # This will work\nwith self.state.mutate(\"my_list\", \"my_dict\"):\n    self.state[\"my_list\"].append(\"spam\")\n    self.state[\"my_dict\"][\"spam\"] = \"eggs\"\n
    More information on reactivity

    For more information on reactivity in PuePy, see the Reactivity Developer Guide.

    "},{"location":"tutorial/03-events/","title":"Counter","text":"

    In this chapter, we introduce event handlers. Take a look at this demo, with plus and minus buttons that increment and decrement a counter. You may remember it from the pupy.dev homepage.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/03_counter/index.htmlEdit

    In this example, we bind two events to event handlers. Follow along with the annotations in the code below for a more detailed explanation:

    counter.py
    from puepy import Application, Page, t\n\napp = Application()\n\n\n@app.page()\nclass CounterPage(Page):\n    def initial(self):\n        return {\"current_value\": 0}\n\n    def populate(self):\n        with t.div(classes=\"button-box\"):\n            t.button(\"-\", \n                     classes=[\"button\", \"decrement-button\"],\n                     on_click=self.on_decrement_click)  # (1)\n            t.span(str(self.state[\"current_value\"]), classes=\"count\")\n            t.button(\"+\", \n                     classes=\"button increment-button\",\n                     on_click=self.on_increment_click)  # (2)\n\n    def on_decrement_click(self, event):\n        self.state[\"current_value\"] -= 1  # (3)\n\n    def on_increment_click(self, event):\n        self.state[\"current_value\"] += 1  # (4)\n\n\napp.mount(\"#app\")\n
    1. The on_click parameter is passed to the button tag, which binds the on_decrement_click method to the button's click event.
    2. The on_click parameter is passed to the button tag, which binds the on_increment_click method to the button's click event.
    3. The on_decrement_click method decrements the current_value key in the page's state.
    4. The on_increment_click method increments the current_value key in the page's state.

    Tip

    The event parameter sent to event handlers is the same as it is in JavaScript. You can call event.preventDefault() or event.stopPropagation() as needed.

    As before, because we are modifying the state directly, the page will re-render automatically. This is the power of PuePy's reactivity system.

    "},{"location":"tutorial/04-refs/","title":"Using Refs","text":"

    Let's introduce our first bug. Try typing a word in the input box in the demo below.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/04_refs_problem/index.htmlEdit

    Notice that as you type, each time the page redraws, your input loses focus. This is because PuePy doesn't know which elements are supposed to \"match\" the ones from the previous refresh, and the ordering is now different. The original <input> is being discarded each refresh and replaced with a new one.

    Now try the fixed version:

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/04_refs_problem/solution.htmlEdit

    Here's the problem code and the fixed code. Notice the addition of a ref= in the fixed version.

    Problem CodeFixed Code
    @app.page()\nclass RefsProblemPage(Page):\n    def initial(self):\n        return {\"word\": \"\"}\n\n    def populate(self):\n        t.h1(\"Problem: DOM elements are re-created\")\n        if self.state[\"word\"]:\n            for char in self.state[\"word\"]:\n                t.span(char, classes=\"char-box\")\n        with t.div(style=\"margin-top: 1em\"):\n            t.input(bind=\"word\", placeholder=\"Type a word\")\n
    @app.page()\nclass RefsSolutionPage(Page):\n    def initial(self):\n        return {\"word\": \"\"}\n\n    def populate(self):\n        t.h1(\"Solution: Use ref=\")\n        if self.state[\"word\"]:\n            for char in self.state[\"word\"]:\n                t.span(char, classes=\"char-box\")\n        with t.div(style=\"margin-top: 1em\"):\n            t.input(bind=\"word\", placeholder=\"Type a word\", ref=\"enter_word\")\n
    "},{"location":"tutorial/04-refs/#using-refs-to-preserve-elements-between-refreshes","title":"Using refs to preserve elements between refreshes","text":"

    To tell PuePy not to garbage collect an element, but to reuse it between redraws, just give it a ref= parameter. The ref should be unique to the component you're coding: that is, each ref should be unique among all elements created in the populate() method you're writing.

    When PuePy finds an element with a ref, it will reuse that ref if it existed in the last refresh, modifying it with any updated parameters passed to it.

    Using references in your code

    The self.refs dictionary is available to you in your page or component. You can access elements by their ref name, like self.refs[\"enter_word\"].

    "},{"location":"tutorial/05-watchers/","title":"Watchers","text":"

    We've introduced reactivity, but what happens when you want to monitor specific variables for changes? In PuePy, you can use on_<variable>_change methods in your components to watch for changes in specific variables. In the example below, try guessing the number 4:

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/05_watchers/index.htmlEdit

    watchers.py
    @app.page()\nclass WatcherPage(Page):\n    def initial(self):\n        self.winner = 4\n\n        return {\"number\": \"\", \"message\": \"\"}\n\n    def populate(self):\n        t.h1(\"Can you guess a number between 1 and 10?\")\n\n        with t.div(style=\"margin: 1em\"):\n            t.input(bind=\"number\", placeholder=\"Enter a guess\", autocomplete=\"off\", type=\"number\", maxlength=1)\n\n        if self.state[\"message\"]:\n            t.p(self.state[\"message\"])\n\n    def on_number_change(self, event):  # (1)\n        try:\n            if int(self.state[\"number\"]) == self.winner:\n                self.state[\"message\"] = \"You guessed the number!\"\n            else:\n                self.state[\"message\"] = \"Keep trying...\"\n        except (ValueError, TypeError):\n            self.state[\"message\"] = \"\"\n
    1. The function name, on_number_change is automatically registered based on the pattern of on_<variable>_change. The event parameter is passed up from the original JavaScript event that triggered the change.

    The watcher method itself changes the self.state[\"message\"] variable based on the value of self.state[\"number\"]. If the number is equal to the self.winner constant, the message is updated to \"You guessed the number!\" Otherwise, the message is set to \"Keep trying...\". The state is once again changed and the page is re-rendered.

    "},{"location":"tutorial/06-components/","title":"Components","text":"

    Components are a way to encapsulate a piece of UI that can be reused throughout your application. In this example, we'll create a Card component and use it multiple times on a page, each time using slots to fill in content.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/06_components/index.htmlEdit

    Component DefinitionComponent Usage
    @t.component()  # (1)!\nclass Card(Component):  # (2)!\n    props = [\"type\", \"button_text\"]  # (3)!\n\n    card = CssClass(  # (4)!\n        margin=\"1em\",\n        padding=\"1em\",\n        background_color=\"#efefef\",\n        border=\"solid 2px #333\",\n    )\n\n    default_classes = [card]\n\n    type_styles = {\n        \"success\": success,\n        \"warning\": warning,\n        \"error\": error,\n    }\n\n    def populate(self):\n        with t.h2(classes=[self.type_styles[self.type]]):\n            self.insert_slot(\"card-header\")    # (5)!\n        with t.p():\n            self.insert_slot()    # (6)!\n        t.button(self.button_text, on_click=self.on_button_click)\n\n    def on_button_click(self, event):\n        self.trigger_event(\"my-custom-event\",\n            detail={\"type\": self.type})  # (7)!\n
    1. The @t.component() decorator registers the class as a component for use elsewhere.
    2. All components should subclass the puepy.Component class.
    3. The props attribute is a list of properties that can be passed to the component.
    4. Classes can be defined programmatically in Python. Class names are automatically generated for each instance, so they're scoped like Python instances.
    5. default_classes is a list of CSS classes that will be applied to the component by default.
    6. The insert_slot method is used to insert content into a named slot. In this case, we are inserting content into the card-header slot.
    7. Unnamed, or default slots, can be filled by calling insert_slot without a name.
    8. trigger_event is used to trigger a custom event. Notice the detail dictionary. This pattern matches the JavaScript CustomEvent API.
    @app.page()\nclass ComponentPage(Page):\n    def initial(self):\n        return {\"message\": \"\"}\n\n    def populate(self):\n        t.h1(\"Components are useful\")\n\n        with t.card(type=\"success\",  # (1)\n                    on_my_custom_event=self.handle_custom_event) as card:  # (2)\n            with card.slot(\"card-header\"):\n                t(\"Success!\")  # (3)\n            with card.slot():\n                t(\"Your operation worked\")  # (4)\n\n        with t.card(type=\"warning\", on_my_custom_event=self.handle_custom_event) as card:\n            with card.slot(\"card-header\"):\n                t(\"Warning!\")\n            with card.slot():\n                t(\"Your operation may not work\")\n\n        with t.card(type=\"error\", on_my_custom_event=self.handle_custom_event) as card:\n            with card.slot(\"card-header\"):\n                t(\"Failure!\")\n            with card.slot():\n                t(\"Your operation failed\")\n\n        if self.state[\"message\"]:\n            t.p(self.state[\"message\"])\n\n    def handle_custom_event(self, event):  # (5)\n        self.state[\"message\"] = f\"Custom event from card with type {event.detail.get('type')}\"\n
    1. The card component is used with the type prop set to \"success\".
    2. The my-custom-event event is bound to the self.handle_custom_event method.
    3. The content for the card-header slot, as defined in the Card component, is populated with \"Success!\".
    4. The default slot is populated with \"Your operation worked\". Default slots are not named.
    5. The handle_custom_event method is called when the my-custom-event event is triggered.
    "},{"location":"tutorial/06-components/#slots","title":"Slots","text":"

    Slots are a way to pass content into a component. A component can define one or more slots, and the calling code can fill in the slots with content. In the example above, the Card component defines two slots: card-header and the default slot. The calling code fills in the slots by calling card.slot(\"card-header\") and card.slot().

    Defining Slots in a componentFilling Slots in the calling code
    with t.h2():\n    self.insert_slot(\"card-header\")\nwith t.p():\n    self.insert_slot()  # (1)\n
    1. If you don't pass a name, it defaults to the main slot
    with t.card() as card:\n    with card.slot(\"card-header\"):\n        t(\"Success!\")\n    with card.slot():\n        t(\"Your operation worked\")\n

    Consuming Slots

    When consuming components with slots, to populate a slot, you do not call t.slot. You call .slot directly on the component instance provided by the context manager:

    with t.card() as card:\n    with card.slot(\"card-header\"):  # (1)\n        t(\"Success!\")\n
    1. Notice card.slot is called, not t.slot or self.slot.
    More information on components

    For more information on components in PuePy, see the Component Developer Guide.

    "},{"location":"tutorial/07-routing/","title":"Routing","text":"

    For single page apps (SPAs) or even complex pages with internal navigation, PuePy's client-side routing feature renders different pages based on the URL and provides a way of linking between various routes. Use of the router is optional and if no router is installed, the application will always render the default page.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/07_routing/index.htmlEdit

    URL Changes

    In the embedded example above, the \"URL\" does not change because the embedded example is not a full web page. In a full web page, the URL would change to reflect the current page. Try opening the example in a new window to see the URL change.

    Inspired by Flask's simple and elegant routing system, PuePy uses decorators on page classes to define routes and parameters. The router can be configured to use either hash-based or history-based routing. Consider this example's source code:

    routing.py
    from puepy import Application, Page, Component, t\nfrom puepy.router import Router\n\napp = Application()\napp.install_router(Router, link_mode=Router.LINK_MODE_HASH)  # (1)\n\npets = {\n    \"scooby\": {\"name\": \"Scooby-Doo\", \"type\": \"dog\", \"character\": \"fearful\"},\n    \"garfield\": {\"name\": \"Garfield\", \"type\": \"cat\", \"character\": \"lazy\"},\n    \"snoopy\": {\"name\": \"Snoopy\", \"type\": \"dog\", \"character\": \"playful\"},\n}\n\n\n@t.component()\nclass Link(Component):  # (2)\n    props = [\"args\"]\n    enclosing_tag = \"a\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        self.add_event_listener(\"click\", self.on_click)\n\n    def set_href(self, href):\n        if issubclass(href, Page):\n            args = self.args or {}\n            self._resolved_href = self.page.router.reverse(href, **args)\n        else:\n            self._resolved_href = href\n\n        self.attrs[\"href\"] = self._resolved_href\n\n    def on_click(self, event):\n        if (\n            isinstance(self._resolved_href, str)\n            and self._resolved_href[0] in \"#/\"\n            and self.page.router.navigate_to_path(self._resolved_href)\n        ):\n            # A page was found; prevent navigation and navigate to page\n            event.preventDefault()\n\n\n@app.page(\"/pet/<pet_id>\")  # (3)\nclass PetPage(Page):\n    props = [\"pet_id\"]\n\n    def populate(self):\n        pet = pets.get(self.pet_id)\n        t.h1(\"Pet Information\")\n        with t.dl():\n            for k, v in pet.items():\n                t.dt(k)\n                t.dd(v)\n        t.link(\"Back to Homepage\", href=DefaultPage)  # (4)\n\n\n@app.page()\nclass DefaultPage(Page):\n    def populate(self):\n        t.h1(\"PuePy Routing Demo: Pet Listing\")\n        with t.ul():\n            for pet_id, pet_details in pets.items():\n                with t.li():\n                    t.link(pet_details[\"name\"],\n                           href=PetPage,\n                           args={\"pet_id\": pet_id})  # (5)\n\n\napp.mount(\"#app\")\n
    1. The router is installed with the link_mode set to Router.LINK_MODE_HASH. This sets the router to use hash-based routing.
    2. The Link component is a custom component that creates links to other pages. It uses the router to navigate to the specified page.
    3. The PetPage class is decorated with a route. The pet_id parameter is parsed from the URL.
    4. The Link component is used to create a link back to the homepage, as passed by the href parameter.
    5. The Link component is used to create links to each pet's page, passing the pet_id as a parameter.
    "},{"location":"tutorial/07-routing/#installing-the-router","title":"Installing the router","text":"

    The router is installed with the install_router method on the application instance:

    app.install_router(Router, link_mode=Router.LINK_MODE_HASH)\n

    If you wanted to use html5 history mode (see the Router developer guide), you would set link_mode=Router.LINK_MODE_HISTORY.

    "},{"location":"tutorial/07-routing/#the-default-page","title":"The default page","text":"

    The default page is rendered for the \"root\" URL or when no URL is specified. The default page is defined with no path:

    @app.page()\nclass DefaultPage(Page):\n    ...\n
    More information on the router

    For more information on the router, see the Router Developer Guide.

    "},{"location":"tutorial/08-pypi-libraries/","title":"Using PyPi Libraries","text":"

    Let's make use of a PyPi library in our project. In this example, we'll use BeautifulSoup to parse an HTML document and actually generate a PuePy component that would render the same content.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/08_libraries/index.htmlEdit

    Small embedded example

    This example may be more useful in a full browser window. Open in new window

    "},{"location":"tutorial/08-pypi-libraries/#using-full-cpythonpyodide","title":"Using Full CPython/Pyodide","text":"

    To make use of a library like BeautifulSoup, we will configure PuePy to use the full CPython/Pyoide runtime, rather than the more minimal MicroPython runtime. This is done by specifying the runtime in the <script> tag in index.html:

    <script type=\"py\" src=\"./libraries.py\" config=\"./pyscript-bs.json\"></script>\n
    "},{"location":"tutorial/08-pypi-libraries/#requiring-packages-from-pypi","title":"Requiring packages from pypi","text":"

    In pyscript-bs.json, we also must specify that we need BeautifulSoup4. This is done by adding it to the packages section of the config file:

    pyscript-bs.json
    {\n  \"name\": \"PuePy Tutorial\",\n  \"debug\": true,\n  \"packages\": [\n    \"./puepy-0.5.0-py3-none-any.whl\",\n    \"beautifulsoup4\"\n  ],\n  \"js_modules\": {\n    \"main\": {\n      \"https://cdn.jsdelivr.net/npm/morphdom@2.7.4/+esm\": \"morphdom\"\n    }\n  }\n}\n

    The type attribute in the PyScript <script> tag can have two values:

    • mpy: Use the MicroPython runtime
    • py: Use the CPython/Pyodide runtime

    See Also

    See also the runtimes developer guide for more information on runtimes.

    Once the dependencies are specified in the config file, we can import the library in our source file:

    from bs4 import BeautifulSoup, Comment\n
    Full Example Source libraries.py
    import re\nfrom bs4 import BeautifulSoup, Comment\nfrom puepy import Application, Page, t\n\napp = Application()\n\nPYTHON_KEYWORDS = [\n    \"false\",\n    \"none\",\n    \"true\",\n    \"and\",\n    \"as\",\n    \"assert\",\n    \"async\",\n    \"await\",\n    \"break\",\n    \"class\",\n    \"continue\",\n    \"def\",\n    \"del\",\n    \"elif\",\n    \"else\",\n    \"except\",\n    \"finally\",\n    \"for\",\n    \"from\",\n    \"global\",\n    \"if\",\n    \"import\",\n    \"in\",\n    \"is\",\n    \"lambda\",\n    \"nonlocal\",\n    \"not\",\n    \"or\",\n    \"pass\",\n    \"raise\",\n    \"return\",\n    \"try\",\n    \"while\",\n    \"with\",\n    \"yield\",\n]\n\n\nclass TagGenerator:\n    def __init__(self, indentation=4):\n        self.indent_level = 0\n        self.indentation = indentation\n\n    def indent(self):\n        return \" \" * self.indentation * self.indent_level\n\n    def sanitize(self, key):\n        key = re.sub(r\"\\W\", \"_\", key)\n        if not key[0].isalpha():\n            key = f\"_{key}\"\n        if key == \"class\":\n            key = \"classes\"\n        elif key.lower() in PYTHON_KEYWORDS:\n            key = f\"{key}_\"\n        return key\n\n    def generate_tag(self, tag):\n        attr_list = [\n            f\"{self.sanitize(key)}={repr(' '.join(value) if isinstance(value, list) else value)}\"\n            for key, value in tag.attrs.items()\n        ]\n\n        underscores_tag_name = tag.name.replace(\"-\", \"_\")\n\n        sanitized_tag_name = self.sanitize(underscores_tag_name)\n        if sanitized_tag_name != underscores_tag_name:\n            # For the rare case where it really just has to be the original tag\n            attr_list.append(f\"tag={repr(tag.name)}\")\n\n        attributes = \", \".join(attr_list)\n\n        return (\n            f\"{self.indent()}with t.{sanitized_tag_name}({attributes}):\"\n            if tag.contents\n            else f\"{self.indent()}t.{sanitized_tag_name}({attributes})\"\n        )\n\n    def iterate_node(self, node):\n        output = []\n        for child in node.children:\n            if child.name:  # Element\n                output.append(self.generate_tag(child))\n                self.indent_level += 1\n                if child.contents:\n                    output.extend(self.iterate_node(child))\n                self.indent_level -= 1\n            elif isinstance(child, Comment):\n                for line in child.strip().split(\"\\n\"):\n                    output.append(f\"{self.indent()}# {line}\")\n            elif isinstance(child, str) and child.strip():  # Text node\n                output.append(f\"{self.indent()}t({repr(child.strip())})\")\n        return output\n\n    def generate_app_root(self, node, generate_full_file=True):\n        header = (\n            [\n                \"from puepy import Application, Page, t\",\n                \"\",\n                \"app = Application()\",\n                \"\",\n                \"@app.page()\",\n                \"class DefaultPage(Page):\",\n                \"    def populate(self):\",\n            ]\n            if generate_full_file\n            else []\n        )\n        self.indent_level = 2 if generate_full_file else 0\n        body = self.iterate_node(node)\n        return \"\\n\".join(header + body)\n\n\ndef convert_html_to_context_manager(html, indent=4, generate_full_file=True):\n    soup = BeautifulSoup(html, \"html.parser\")\n    generator = TagGenerator(indentation=indent)\n    return generator.generate_app_root(soup, generate_full_file=generate_full_file)\n\n\n@app.page()\nclass DefaultPage(Page):\n    def initial(self):\n        return {\"input\": \"\", \"output\": \"\", \"error\": \"\", \"generate_full_file\": True}\n\n    def populate(self):\n        with t.div(classes=\"section\"):\n            t.h1(\"Convert HTML to PuePy syntax with BeautifulSoup\", classes=\"title is-1\")\n            with t.div(classes=\"columns is-variable is-8 is-multiline\"):\n                with t.div(classes=\"column is-half-desktop is-full-mobile\"):\n                    with t.div(classes=\"field\"):\n                        t.div(\"Enter HTML Here\", classes=\"label\")\n                        t.textarea(bind=\"input\", classes=\"textarea\")\n                with t.div(classes=\"column is-half-desktop is-full-mobile\"):\n                    with t.div(classes=\"field\"):\n                        t.div(\"Output\", classes=\"label\")\n                        t.textarea(bind=\"output\", classes=\"textarea\", readonly=True)\n            with t.div(classes=\"field is-grouped\"):\n                with t.p(classes=\"control\"):\n                    t.button(\"Convert\", classes=\"button is-primary\", on_click=self.on_convert_click)\n                with t.p(classes=\"control\"):\n                    with t.label(classes=\"checkbox\"):\n                        t.input(bind=\"generate_full_file\", type=\"checkbox\")\n                        t(\" Generate full file\")\n            if self.state[\"error\"]:\n                with t.div(classes=\"notification is-danger\"):\n                    t(self.state[\"error\"])\n\n    def on_convert_click(self, event):\n        self.state[\"error\"] = \"\"\n        try:\n            self.state[\"output\"] = convert_html_to_context_manager(\n                self.state[\"input\"], generate_full_file=self.state[\"generate_full_file\"]\n            )\n        except Exception as e:\n            self.state[\"error\"] = str(e)\n\n\napp.mount(\"#app\")\n

    PyScript documentation on packages

    For more information, including packages available to MicroPython, refer to the PyScript docs.

    "},{"location":"tutorial/09-using-web-components/","title":"Using Web Components","text":"

    Web Components are a collection of technologies, supported by all modern browsers, that let developers reuse custom components in a framework-agnostic way. Although PuePy is an esoteric framework, and no major component libraries exist for it (as they do with React or Vue), you can use Web Component widgets easily in PuePy and make use of common components available on the Internet.

    "},{"location":"tutorial/09-using-web-components/#using-shoelace","title":"Using Shoelace","text":"

    Shoelace is a popular and professionally developed suite of web components for building high quality user experiences. In this example, we'll see how to use Shoelace Web Components inside a project of ours. Here is a working example:

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/09_webcomponents/index.htmlEdit

    "},{"location":"tutorial/09-using-web-components/#adding-remote-assets","title":"Adding remote assets","text":"

    First, we'll need to load Shoelace from its CDN in our HTML file:

    index.html
    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/light.css\"/>\n<script type=\"module\"\n      src=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace-autoloader.js\"></script>\n
    "},{"location":"tutorial/09-using-web-components/#using-web-components-in-python","title":"Using Web Components in Python","text":"

    Because WebComponents are initialized just like other HTML tags, they can be used directly in Python:

    @app.page()\nclass DefaultPage(Page):\n    def populate(self):\n        with t.sl_dialog(label=\"Dialog\", classes=\"dialog-overview\", tag=\"sl-dialog\", ref=\"dialog\"):  # (1)!\n            t(\"Web Components are just delightful.\")\n            t.sl_button(\"Close\", slot=\"footer\", variant=\"primary\", on_click=self.on_close_click)  # (2)!\n        t.sl_button(\"Open Dialog\", tag=\"sl-button\", on_click=self.on_open_click)\n\n    def on_open_click(self, event):\n        self.refs[\"dialog\"].element.show()\n\n    def on_close_click(self, event):\n        self.refs[\"dialog\"].element.hide()\n
    1. The sl_dialog tag is a custom tag that creates a sl-dialog Web Component. It was defined by the Shoelace library we loaded via CDN. 2. The sl_button tag is another custom tag that creates a sl-button Web Component.

    "},{"location":"tutorial/09-using-web-components/#access-methods-and-properties-of-web-components","title":"Access methods and properties of web components","text":"

    Web Components are meant to be access directly, like this in JavaScript:

    <sl-dialog id=\"foo\"></sl-dialog>\n\n<script>\n    document.querySelector(\"#foo\").show()\n</script>\n

    The actual DOM elements are accessible in PuePy, but require using the .element attribute of the higher level Python instance of your tag:

    self.refs[\"dialog\"].element.show()\n
    "},{"location":"tutorial/10-full-app/","title":"A Full App Template","text":"

    Let's put together what we've learned so far. This example is an app with routing, a sidebar, and a handful of pages.

    https://kkinder.pyscriptapps.com/puepy-tutorial/latest/tutorial/10_full_app/index.htmlEdit

    URL Changes

    In the embedded example above, the \"URL\" does not change because the embedded example is not a full web page. In a full web page, the URL would change to reflect the current page. Try opening the example in a new window to see the URL change.

    "},{"location":"tutorial/10-full-app/#project-layout","title":"Project layout","text":"

    The larger example separates logic out into several files.

    • main.py: The Python file started from our <script> tag
    • common.py: A place to put objects common to other files
    • components.py: A place to put reusable components
    • pages.py: A place to put individual pages we navigate to
    "},{"location":"tutorial/10-full-app/#configuring-pyscript-for-multiple-files","title":"Configuring PyScript for multiple files","text":"

    To make additional source files available in the Python runtime environment, add them to the files list in the PyScript configuration file:

    pyscript-app.json
    {\n  \"name\": \"PuePy Tutorial\",\n  \"debug\": true,\n  \"files\": {\n    \"./common.py\": \"common.py\",\n    \"./components.py\": \"components.py\",\n    \"./main.py\": \"main.py\",\n    \"./pages.py\": \"pages.py\"\n  },\n  \"js_modules\": {\n    \"main\": {\n      \"https://cdn.jsdelivr.net/npm/chart.js\": \"chart\",\n      \"https://cdn.jsdelivr.net/npm/morphdom@2.7.4/+esm\": \"morphdom\"\n    }\n  },\n  \"packages\": [\n    \"../../puepy-0.6.2-py3-none-any.whl\"\n  ]\n}\n
    "},{"location":"tutorial/10-full-app/#adding-chartjs","title":"Adding Chart.js","text":"

    We also added a JavaScript library, chart.js, to the project.

      \"https://cdn.jsdelivr.net/npm/chart.js\": \"chart\"\n

    JavaScript Modules

    See JavaScript Modules in PyScript's documentation for additional information on loading JavaScript libraries into your project.

    We use charts.js directly from Python, in components.py:

    @t.component()\nclass Chart(Component):\n    props = [\"type\", \"data\", \"options\"]\n    enclosing_tag = \"canvas\"\n\n    def on_redraw(self):\n        self.call_chartjs()\n\n    def on_ready(self):\n        self.call_chartjs()\n\n    def call_chartjs(self):\n        if hasattr(self, \"_chart_js\"):\n            self._chart_js.destroy()\n\n        self._chart_js = js.Chart.new(\n            self.element,\n            jsobj(type=self.type, data=self.data, options=self.options),\n        )\n

    We call the JavaScript library in two places. When the component is added to the DOM (on_ready) and when it's going to be redrawn (on_redraw).

    "},{"location":"tutorial/10-full-app/#reusing-code","title":"Reusing Code","text":""},{"location":"tutorial/10-full-app/#a-common-app-layout","title":"A common app layout","text":"

    In components.py, we define a common application layout, then reuse it in multiple pages:

    class SidebarItem:\n    def __init__(self, label, icon, route):\n        self.label = label\n        self.icon = icon\n        self.route = route\n\n\n@t.component()\nclass AppLayout(Component):\n    sidebar_items = [\n        SidebarItem(\"Dashboard\", \"emoji-sunglasses\", \"dashboard_page\"),\n        SidebarItem(\"Charts\", \"graph-up\", \"charts_page\"),\n        SidebarItem(\"Forms\", \"input-cursor-text\", \"forms_page\"),\n    ]\n\n    def precheck(self):\n        if not self.application.state[\"authenticated_user\"]:\n            raise exceptions.Unauthorized()\n\n    def populate(self):\n        with t.sl_drawer(label=\"Menu\", placement=\"start\", classes=\"drawer-placement-start\", ref=\"drawer\"):\n            self.populate_sidebar()\n\n        with t.div(classes=\"container\"):\n            with t.div(classes=\"header\"):\n                with t.div():\n                    with t.sl_button(classes=\"menu-btn\", on_click=self.show_drawer):\n                        t.sl_icon(name=\"list\")\n                t.div(\"The Dapper App\")\n                self.populate_topright()\n            with t.div(classes=\"sidebar\", id=\"sidebar\"):\n                self.populate_sidebar()\n            with t.div(classes=\"main\"):\n                self.insert_slot()\n            with t.div(classes=\"footer\"):\n                t(\"Business Time!\")\n\n    def populate_topright(self):\n        with t.div(classes=\"dropdown-hoist\"):\n            with t.sl_dropdown(hoist=\"\"):\n                t.sl_icon_button(slot=\"trigger\", label=\"User Settings\", name=\"person-gear\")\n                with t.sl_menu(on_sl_select=self.on_menu_select):\n                    t.sl_menu_item(\n                        \"Profile\",\n                        t.sl_icon(slot=\"suffix\", name=\"person-badge\"),\n                        value=\"profile\",\n                    )\n                    t.sl_menu_item(\"Settings\", t.sl_icon(slot=\"suffix\", name=\"gear\"), value=\"settings\")\n                    t.sl_divider()\n                    t.sl_menu_item(\"Logout\", t.sl_icon(slot=\"suffix\", name=\"box-arrow-right\"), value=\"logout\")\n\n    def on_menu_select(self, event):\n        if event.detail.item.value == \"logout\":\n            self.application.state[\"authenticated_user\"] = \"\"\n\n    def populate_sidebar(self):\n        for item in self.sidebar_items:\n            with t.div():\n                with t.sl_button(\n                    item.label,\n                    variant=\"text\",\n                    classes=\"sidebar-button\",\n                    href=self.page.router.reverse(item.route),\n                ):\n                    if item.icon:\n                        t.sl_icon(name=item.icon, slot=\"prefix\")\n\n    def show_drawer(self, event):\n        self.refs[\"drawer\"].element.show()\n
    "},{"location":"tutorial/10-full-app/#loading-indicator","title":"Loading indicator","text":"

    Since CPython takes a while to load on slower connections, we'll populate the <div id=\"app> element with a loading indicator, which will be replaced once the application mounts:

    <div id=\"app\">\n  <div style=\"text-align: center; height: 100vh; display: flex; justify-content: center; align-items: center;\">\n    <sl-spinner style=\"font-size: 50px; --track-width: 10px;\"></sl-spinner>\n  </div>\n</div>\n
    "},{"location":"tutorial/10-full-app/#further-experimentation","title":"Further experimentation","text":"

    Don't forget to try cloning and modifying all the examples from this tutorial on PyScript.com.

    "}]} \ No newline at end of file diff --git a/development/sitemap.xml b/development/sitemap.xml index c4a9081..a0cc993 100644 --- a/development/sitemap.xml +++ b/development/sitemap.xml @@ -2,126 +2,126 @@ https://docs.puepy.dev/stable/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/faq/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/installation/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/cookbook/loading-indicators/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/cookbook/navigation-guards/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/guide/advanced-routing/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/guide/css-classes/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/guide/in-depth-components/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/guide/pyscript-config/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/guide/reactivity/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/guide/runtimes/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/includes/abbreviations/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/reference/application/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/reference/component/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/reference/exceptions/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/reference/prop/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/reference/reactivity/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/reference/router/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/reference/storage/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/reference/tag/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/tutorial/00-using-this-tutorial/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/tutorial/01-hello-world/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/tutorial/02-hello-name/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/tutorial/03-events/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/tutorial/04-refs/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/tutorial/05-watchers/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/tutorial/06-components/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/tutorial/07-routing/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/tutorial/08-pypi-libraries/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/tutorial/09-using-web-components/ - 2025-02-16 + 2025-02-22 https://docs.puepy.dev/stable/tutorial/10-full-app/ - 2025-02-16 + 2025-02-22 \ No newline at end of file diff --git a/development/sitemap.xml.gz b/development/sitemap.xml.gz index efbd623..9294c7d 100644 Binary files a/development/sitemap.xml.gz and b/development/sitemap.xml.gz differ diff --git a/development/tutorial/01-hello-world/index.html b/development/tutorial/01-hello-world/index.html index 42bbdba..fe0c7cf 100644 --- a/development/tutorial/01-hello-world/index.html +++ b/development/tutorial/01-hello-world/index.html @@ -1419,7 +1419,7 @@

    Hello, World! 0.6.2

    "files": {}, "js_modules": { "main": { - "https://cdn.jsdelivr.net/npm/morphdom@2.7.2/+esm": "morphdom" + "https://cdn.jsdelivr.net/npm/morphdom@2.7.4/+esm": "morphdom" } }, "packages": [ diff --git a/development/tutorial/08-pypi-libraries/index.html b/development/tutorial/08-pypi-libraries/index.html index 42a2982..c0597c8 100644 --- a/development/tutorial/08-pypi-libraries/index.html +++ b/development/tutorial/08-pypi-libraries/index.html @@ -1343,7 +1343,7 @@

    Requiring packages from pypi

    ], "js_modules": { "main": { - "https://cdn.jsdelivr.net/npm/morphdom@2.7.2/+esm": "morphdom" + "https://cdn.jsdelivr.net/npm/morphdom@2.7.4/+esm": "morphdom" } } } diff --git a/development/tutorial/10-full-app/index.html b/development/tutorial/10-full-app/index.html index 83c4e53..3f30b31 100644 --- a/development/tutorial/10-full-app/index.html +++ b/development/tutorial/10-full-app/index.html @@ -1464,7 +1464,7 @@

    Configuring PyScript for multip "js_modules": { "main": { "https://cdn.jsdelivr.net/npm/chart.js": "chart", - "https://cdn.jsdelivr.net/npm/morphdom@2.7.2/+esm": "morphdom" + "https://cdn.jsdelivr.net/npm/morphdom@2.7.4/+esm": "morphdom" } }, "packages": [