PuePy: Frontend Framework for PyScript

repository·main·Indexed 43 days ago

https://github.com/kkinder/puepy

A lightweight frontend framework for PyScript that enables Python-based web development directly in the browser via WebAssembly. PuePy 0.6.5 features reactive data binding, a component-based architecture, and a built-in SPA router, supporting both Pyodide and MicroPython runtimes without the need for JavaScript build tools.

Tokens
22.3K
Snippets
81
Records
101
Agent score
55%

What's inside PuePy

  1. Overview of PuePy frontend framework

    main

    PuePy is a Python-first frontend web framework that runs directly in the browser using WebAssembly via PyScript. It is inspired by Vue.js but is built entirely in Python, eliminating the need for JavaScript-based build tools like NPM, Yarn, Webpack, or Vite.

    Key features include:

    • Reactivity: Automatic UI redraws when component state changes.
    • Component-Based Design: Reusable units that encapsulate data, logic, and presentation.
    • Single-Class Components: Each component and page is defined as a single Python class (inspired by Vue's Single File Components).
    • Vue-inspired API: Supports Events, Slots, and Props.
    • Pythonic Design: Prioritizes Pythonic conventions and minimal verbosity.
  2. What is PuePy and how does it work?

    main

    PuePy is a lightweight web framework designed for PyScript that enables running Python directly in the browser via WebAssembly. It provides a modern web development experience without the need for JavaScript build tools like Webpack or NPM.

    Key features include:

    • Reactive data binding: Automatically updates the UI when state changes.
    • Component-based architecture: Build UI using structured components.
    • SPA Router: Includes a built-in Single Page Application router.
    • No build layer: Python code is executed directly in the browser.
    • Runtime flexibility: Supports both Pyodide and Micropython runtimes.
  3. Pass HTML Attributes to components

    main

    Any keyword arguments passed to a component that do not match a defined prop are treated as attributes. These are stored in self.attrs and rendered directly onto the component's HTML element in the DOM.

    When to use props vs attributes:

    • Props: Use when the data is needed for the component's internal logic or rendering.
    • Attributes: Use when the data is intended for the rendered HTML (e.g., id, placeholder, class) but not used by the component's Python logic.
    from puepy import Component, Page, t
    
    class NameInput(Component):
        enclosing_tag = "input"
    
    class MyPage(Page):
        def populate(self):
            # 'placeholder' is not a prop, so it is passed as an attribute
            t.name_input(id="name_input", placeholder="Enter your name")
  4. How PuePy routing works

    main

    PuePy's routing system allows for Single Page Application (SPA) behavior by rendering different Page components based on the current URL.

    • Optionality: Routing is optional. If no router is installed via app.install_router(), the application will always render the default page (the one defined with @app.page()).
    • Routing Modes: The router supports both LINK_MODE_HASH and LINK_MODE_HISTORY.
    • Parameter Mapping: URL segments defined with <name> in the @app.page decorator are automatically mapped to the Page instance's properties, provided those names are included in the class's props list.
    • Link Resolution: When using a router-aware link component, passing a Page class to the href attribute allows the router to resolve the actual URL path (via router.reverse) and handle the navigation event without a full page reload.
  5. How to create components in PuePy

    main

    In PuePy, you should not instantiate puepy.Component directly. Instead, components are created within a populate() method by calling t.tag_name(). This ensures the component is correctly integrated into the framework's lifecycle and rendering system. Direct instantiation of a component instance is not supported.

    # Do NOT do this:
    # my_comp = Component()
    
    # DO this inside your populate() method:
    # my_comp = t.tag_name()
  6. How slots work in PuePy

    main

    Slots allow components to accept and render content provided by their callers.

    Defining Slots (Inside the Component)

    Inside the component's populate method, use self.insert_slot(name) to designate where content should appear.

    • Named Slots: self.insert_slot("slot-name") creates a specific insertion point.
    • Default Slot: self.insert_slot() (without a name) creates the primary/unnamed slot.

    Filling Slots (In the Caller)

    When using a component, you must use the component instance provided by the as keyword in a with statement to access slots.

    Important: Do not use t.slot(). You must call .slot() on the component instance itself.

    # Correct way to fill slots
    with t.card() as card:
        with card.slot("card-header"):
            t("This goes in the header")
        with card.slot():
            t("This goes in the default slot")
  7. How reactivity works and how to handle deep mutations

    main

    PuePy uses a ReactiveDict for self.state. When state values change, the component automatically redraws to update the DOM.

    Important Limitation: PuePy cannot automatically detect "deep" changes made to complex objects like lists or dictionaries if they are modified in-place.

    Incorrect approach:

    self.state["my_list"].append("spam")
    self.state["my_dict"]["spam"] = "eggs"

    Correct approach: To perform in-place mutations on nested objects, you must use the self.state.mutate() context manager, specifying the keys that are being changed. This notifies the reactivity system of the updates.

    # This will work
    with self.state.mutate("my_list", "my_dict"):
        self.state["my_list"].append("spam")
        self.state["my_dict"]["spam"] = "eggs"
  8. Use `mutate()` for in-place updates to mutable objects

    main

    PuePy's reactivity relies on dictionary __setitem__ and __delitem__ methods. It cannot detect changes made to mutable objects (like lists or dicts) inside the state if they are modified in-place (e.g., using .append()).

    To ensure the UI refreshes after an in-place modification, wrap the change in a self.state.mutate(*keys) context manager. This also allows you to batch multiple changes so that listeners are only called once after the context manager exits.

    class MyComponent(Component):
        def update_movies(self):
            # Use mutate to notify PuePy of changes to the 'movies' key
            with self.state.mutate("movies"):
                self.state["movies"].append("Monty Python’s Life of Brian")
  9. Navigate parent/child relationships in components

    main

    Components and tags exist in a hierarchy. You can navigate this hierarchy using the following attributes (do not modify these directly):

    • self.parent: The direct parent Tag instance.
    • self.page: The Page instance responsible for rendering this component.
    • self.origin: The Component or Page instance that created this component in its populate() method.
    • self.children: A list of direct child nodes (available on parent instances).
    • self.refs: A dictionary of instances created during the most recent populate() call (available on parent instances).
  10. Choose between Pyodide and MicroPython runtimes

    main

    PuePy supports two runtime environments inherited from PyScript. Choosing the right one depends on your requirements for package support, feature completeness, and initial page load performance.

    Pyodide (type="py")

    Use Pyodide when you need a full Python environment. It is best for:

    • Using PyPi packages.
    • Accessing the full Python standard library.
    • Using advanced Python features (e.g., Python 3.9+).
    • Using type hinting.
    • Trade-off: It has a larger runtime (~11MB), which increases initial load time.

    MicroPython (type="mpy")

    Use MicroPython when performance and speed are priorities. It is best for:

    • Scenarios where webpage response time and initial load speed are critical.
    • Simple Python logic used only for reactivity and interactivity.
    • Trade-off: It has a much smaller runtime (~170k), but lacks many standard library features, PyPi package access, and robust type hinting.
  11. Monitor variable changes with Watchers

    main

    In PuePy, you can monitor specific state variables for changes by defining methods within your component using the naming pattern on_<variable>_change.

    When the variable bound to the UI changes (e.g., via an input field), PuePy automatically calls the corresponding watcher method. The method receives an event parameter, which is the original JavaScript event that triggered the change. Inside the watcher, you can perform logic and update other parts of self.state, which will trigger a re-render of the page.

    @app.page()
    class WatcherPage(Page):
        def initial(self):
            self.winner = 4
            return {"number": "", "message": ""}
    
        def populate(self):
            t.input(bind="number", type="number")
            if self.state["message"]:
                t.p(self.state["message"])
    
        def on_number_change(self, event):
            # This method is called whenever self.state["number"] changes
            try:
                if int(self.state["number"]) == self.winner:
                    self.state["message"] = "You guessed the number!"
                else:
                    self.state["message"] = "Keep trying..."
            except (ValueError, TypeError):
                self.state["message"] = ""
  12. Use Slots to inject content into components

    main

    Slots allow parent components to inject content (tags, other components, etc.) into specific locations within a child component.

    • Defining Slots: Use self.insert_slot within the populate() method of a component or page.
    • Consuming Slots: When using the component, use a context manager and the .slot() method on the component instance to specify where the injected content goes.