Git Product home page Git Product logo

mesop's Introduction

Mesop: Build delightful web apps quickly in Python πŸš€

Used at Google for rapid internal app development

Mesop is a Python-based UI framework that allows you to rapidly build web apps like demos and internal apps:

Intuitive for UI novices ✨

  • Write UI in idiomatic Python code
  • Easy to understand reactive UI paradigm
  • Ready to use components

Frictionless developer workflows 🏎️

  • Hot reload so the browser automatically reloads and preserves state
  • Rich IDE support with strong type safety

Flexible for delightful demos 🀩

  • Build custom UIs without writing Javascript/CSS/HTML
  • Compose your UI into components, which are just Python functions

Write your first Mesop app in less than 10 lines of code...

Demo app

import time

import mesop as me
import mesop.labs as mel


@me.page(path="/text_to_text", title="Text I/O Example")
def app():
  mel.text_to_text(
    upper_case_stream,
    title="Text I/O Example",
  )


def upper_case_stream(s: str):
  yield s.capitalize()
  time.sleep(0.5)
  yield "Done"

Try it

Colab

You can try Mesop on Colab!

Locally

Step 1: Install it

$ pip install mesop

Step 2: Copy the example above into main.py

Step 3: Run the app

$ mesop main.py

Learn more in Getting Started.

Disclaimer

This is not an officially supported Google product.

mesop's People

Contributors

dependabot[bot] avatar djm93dev avatar eltociear avatar orangerd avatar rczhang avatar richard-to avatar t-zaid avatar wwwillchen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mesop's Issues

Remove `me.on` wrapper for event handlers

There's two purposes for the me.on decorator now:

  1. register the functions with a the qualified function name (module + fn name) so it can be called to process an event update

  2. maps from the general UserEvent type to the specific event type needed

  3. can be pretty easily addressed because we know the event type based on the callsite usage.
    For 1), one idea is to first do a trace run, which executes the render loop (without actually rendering) to: a) collect references to all functions and, potentially, b) to compute the initial component tree so we can do a diff and and only send the delta to the client.

I need a reliable way of fingerprinting a function so that if there's two functions with the same name, I'll call the correct one.

Create `mesop` CLI to support Bazel-agnostic hot reload

After supporting pip install mesop (#41), one of the issues with running Mesop on pip is that it doesn't support hot reload. Right now, Mesop's hot reload mechanism relies on ibazel which injects the livereload script and sends a rebuild event.

Questions:

  • How do we trigger a rebuild? Whenever an app file changes. This isn't going to be as good as ibazel where there's a proper dependency graph. We will do a simple heuristic if any file within a directory or sub-directory of the main module is changed, then we will reload everything. This is the same heuristic used inside
    def get_app_modules(
  • How do we send an event? Ideally, we will use websockets, otherwise we can do some sort of polling on the client-side.

Tasks:

Prior Art

Streamlit

Gradio

Home-grown

Basically create a while loop which checks file modified time.

    last_modified = os.path.getmtime(module_path)
    while True:
        # Get the current modification time
        current_modified = os.path.getmtime(module_path)

        # Compare the current modification time with the last modification time
        if current_modified != last_modified:
            # Update the last modification time
            last_modified = current_modified
            module = read_module(module_path)
            if module is not None:
                try:
                    runtime.set_module(module)
                    runtime.reload_all_sessions()
                except Exception as e:
                    runtime.log(
                        pb.ServerLog(
                            message=f"Hot reload error: Module '{module_path}' has {e}.\n${traceback.format_exc()}",
                            level=pb.ServerLog.ERROR,
                        )
                    )

        # Wait for 1 second before checking again
        await asyncio.sleep(1)

Support on_load event

Sometimes you may want to fetch data or anything expensive / async for the initial screen.

There's no good way to do this in Mesop today, so you'd need to either a) wait until the user interacts (e.g. clicks a button) or b) do it as part of a navigation from a previous page.

Considerations

  • Configure at the top page-level component or any component?

Only load dev tools when in dev mode

Goals:

  • Don't load unnecessary module for production builds
  • Don't show dev tools for non-developers

How:

  • extract devtools-related hooks into a higher component, e.g. DevToolsApps
  • create a new js_binary (e.g. app_with_dev_tools)
  • create a flag in cli.py which decides whether to swap the regular bundle for the bundle with dev tools

Support custom (Python) components

This is focused on allowing app developers to create their own custom components which are built on top of other components.

We will track in a separate issue the work to support custom native components.

  • Editor support: can create new custom component instances
  • DevTools support: display in component tree
  • Allow composite custom components (unify composite and non-composite API)

Support hot reloading with CLI

Goal: Provide hot reloading for developers using Optic.

Desired behavior: When an Optic application is modified under development mode, we will send a signal from the server to client to save the client state to a persistent web storage (e.g localStorage) and trigger a reload.

Design notes

ibazel (https://github.com/bazelbuild/bazel-watcher) provides a helpful wrapper around bazel which watches for filesystem changes and triggers a livereload.

Prior Art

TODO:

  • Within cli.py, detect whether IBAZEL_LIVERELOAD_URL environmental variable is present and use it to inject a script in index.html.
  • scan stdin for commands golang example.

Potential implementation:

def monitor_stdin():
    while True:
        line = sys.stdin.readline().strip()
        if line == "SOURCE_CHANGE":
            reexecute()

stdin_thread = threading.Thread(target=monitor_stdin)
stdin_thread.daemon = True
stdin_thread.start()
  • Add the following tags to Bazel target:
 tags = [
        # This tag starts the live_reload server inside iBazel and instructs it to send reload events to webbrowsers.
        "ibazel_live_reload",
        # This tag instructs ibazel to pipe into stdin a event describing actions.
        "ibazel_notify_changes",
    ],

Provide a strongly-typed styles API

Right now the styles API is basically a string we send directly from Python to Angular.

Motivation

By providing a strongly-typed, first-class styles API, we can solve several issues:

  • Improve DevEx - instead of catching issues (e.g. typos; malformed styles) in the browser (DevTools), we can catch them early-on, with static type-checking.
  • Advanced capabilities - support things like media queries and pseudo selectors (e.g. :hover).
  • Improved performance - by only sending in the values via protos, we can reduce the network payload.

Prior art

  • https://stylexjs.com/ - Provides a simple, but powerful styling API that relies on AOT-compilation. Because of this, I think this is a good model to look towards, particularly because this will encourage patterns that are more amenable to UI-driven code-edits as part of #31.
  • https://tailwindcss.com/ - Utility-class CSS library which allows for flexible things like group and media queries.

Migrate Python server framework to bottle.py

bottle.py has less dependencies than Flask which makes it easier to sync downstream.

  • WSGI-compatible
  • Good documentation

Experience using Flask:

I haven't used Flask extensively, but in the ~month or so developing with Flask, I have found it quite un-intuitive in several aspects. For example, the docs says:

Threaded mode is enabled by default.

Rather, than making it explicitly there's an option called threaded, you need to read through the Werkzeug's docs to understand this property. In general, the delineation between Flask and Werkzeug seems a bit fuzzy and makes reading the docs and understanding Flask harder. Other people have reported issues with using Flask.

Alternatives:

I considered other frameworks such as Tornado however it doesn't support running a Tornado server in a WSGI container docs

Bottle.py usage notes

Example usage:

context = request.environ.get('context')

Support Python to JS FFI

Use case

Sometimes there's a function we need to call in JavaScript from our Python code.

Example

JS API:

window.foo = (input: str): str => {
  return input + " processed";
}

Python API:

import mesop as me

def on_click():
  value = await me.call_js("foo", ["arg1"])
  print(value) # should be "foo processed"

How it works

  • call_js sends a message to client-side
  • once response is evaluated on client side, it's returned to python

Dynamically render component from ComponentSpec

Motivation

It's tedious to manually create wrappers for native Angular components even with a component generator script, particularly because some components may only exist internally in our downstream codebase.

We will still provide a way to manually specify components the current way, for advanced use cases, but for >80% of use cases, we should be able to code-gen the component.

Challenges

One of the difficulties is that it can produce difficult to understand errors

Design

Outputs

What it needs to generate?

  • Angular:
    • Template
    • Component renderer
  • Python:
    • Component API
    • Export symbol in mesop/__init__.py
  • Tests (although, this is less important if everything is code-generated)

Inputs

  • Python API
  • Angular component source

How

Use TS compiler (or just parse AST) of the specified Angular component.
Generate a ComponentNgSpec based on the input and output. This will turn into a proto. We can use this proto to: 1) generate the Python file (may or may not be checked-in) and 2) use it to dynamically resolve the Angular component.

import { Injectable, ViewContainerRef, ComponentRef } from '@angular/core';
import { YourComponent } from './path-to-your-component';

@Injectable({
  providedIn: 'root'
})
export class DynamicComponentService {
  constructor(private componentFactoryResolver: ComponentFactoryResolver) {}

  createComponent(viewContainerRef: ViewContainerRef): ComponentRef<YourComponent> {
    const componentRef = viewContainerRef.createComponent(SomeComponent);

    // Optional: Interact with the component
    // componentRef.instance.someInput = 'some value';
    // componentRef.instance.someOutput.subscribe(() => { ... });

    return componentRef;
  }
}

Steps:

  • Do DynamicComponentService, but manually write the code.
  • Specify ComponentSpec protocol to interact with DynamicComponentService
  • Write protocol data by hand and have DynamicComponentService programmatically use ComponentSpec data
  • Make sure compiler optimizations don't break the above steps
  • Generate Python class based on ComponentSpec
  • Generate ComponentSpec using TS compiler (AST traversal)
  • Create CLI to generate ComponentSpec using Angular Component TS source file as input

Fix loading indicator (for processing events)

The loading indicator should always be visible (regardless if the viewport has been scrolled) and it should not bump the content. It should probably be done with a position: absolute (but I'm not sure if we should create some margin, otherwise it will make it hard to style that area.

Clean up component documentation/terminology

Right now I refer to the concept of a component which accepts a child as a Composite Component in the docs, but I've named the variations of components as content_{component} (e.g. content_checkbox, content_button).

For consistency, this should always be referred to as Content Component.

Visual editor

Motivation

One of the challenges with developing Mesop applications for Python engineers with limited FE experience is that creating layouts and dealing with styling has a substantive learning curve.

By providing a visual editor, we can lower the barrier to entry and improve the velocity of Mesop app developers.

Prior Art

There's many similar attempts, but perhaps the most relevant example is Puck which is an open-source visual editor for React; it provides a very intuitive user experience. One of the differences, though, is that Puck emits JSON (a data structure of the component tree), but I'd like to emit Python code to close the gap between code produced by the visual editor vs. hand-written.

Design

High-level component structure

  • Component library (left sidenav) - user can drag a component from the left sidenav and drop it into the center "canvas" area.
  • Canvas area (main center panel) - this is the droppable area. Also, you can select a component to focus on it.
  • Outline - similar to the existing component treeΒ in Mesop Dev Tools, this displays the component hierarchy
  • Editor (right sidenav) - this provides form controls for editing the focused component's value.

Interfaces

Server -> client

// Put in RenderEvent
message ComponentConfig {
  string component_name
  repeated EditorField fields
  string category
}

message EditorField {
  string name
  ParamType type
}

message ParamType {
   oneof type {
      BoolType bool
      IntType int
      StringType string
      StringLiteralType string_literal
      ListType list
      StructType struct
   }
}

message BoolType {
   bool default_value
}

message StructType {
   repeated EditorField fields
}

message IntType {
  int32 default_value
}

message StringType {
  string default_value
}

message StringLiteralType {
   repeated string literals // note: defaults to first element
}

message ListType {
  ParamType type
}

From client to server

  • update_component_code(source_code_location, component_edit)
message SourceCodeLocation {
  string  module
  int32 line
  int32 col
}

message ComponentEdit {
  SourceCodeLocation location
  string keyword_argument (can be empty, if it's a positional argument)
  string new_value
}

Generating ComponentConfig proto from Python API

Example program:

import inspect
from typing import Callable, Any, Literal

# The button function (for reference)
def button(
    *,
    on_click: Callable[[ClickEvent], Any] | None = None,
    type: Literal["raised", "flat", "stroked", "icon"] | None = None,
    color: str = "",
    disable_ripple: bool = False,
    disabled: bool = False,
    key: str | None = None,
):
    pass

# Function to analyze and serialize the function signature
def serialize_function_to_proto(func):
    sig = inspect.signature(func)
    component_config = {
        "component_name": func.__name__,
        "fields": [],
        "category": "UI Components"  # Example category
    }

    for name, param in sig.parameters.items():
        editor_field = {"name": name}
        param_type = param.annotation

        # Map Python types to proto ParamType
        if param_type in [bool]:
            editor_field["type"] = {"bool": {"default_value": param.default}}
        elif param_type in [str]:
            editor_field["type"] = {"string": {"default_value": param.default}}
        elif param_type == Literal["raised", "flat", "stroked", "icon"]:
            editor_field["type"] = {"string_literal": {"literals": ["raised", "flat", "stroked", "icon"]}}
        # Add more type mappings as needed...

        component_config["fields"].append(editor_field)

    return component_config

# Serialize the button function
serialized_button = serialize_function_to_proto(button)
print(serialized_button)

Flows

When you drag a component, it will create a placeholder component and emit a user event. EditorEvent. Inside EditorEvent, there's three types: 1) create, 2) edit, and 3) delete. After the user event, we will do a hot reload.

  • User drags component from component library into canvas
  • User triggers a UserEvent of type EditorEvent (which has three sub-types: 1) create, 2) edit, and 3) delete)
  • Server checks if dev mode is on. If it's off, then no-op.
  • Server modifies source code.
  • This triggers hot reload

Implementation notes

Tasks

  • Refactor Mesop so that there's three modes/apps: dev, editor and prod. Create a flag for cli.py (--prod) which determines which binary to use.
  • Rename DevToolsService -> EditorService and have two versions: a no-op version when in dev mode; and a real impl for editor mode (injected by the root apps).
  • Create basic shell UI: ComponentLibrary component for left sidenav and ComponentFieldsEditor and ComponentOutline components for right sidenav
  • Fix the top padding thing
  • Create a nice background overlay of selected component
  • Attach source location to component instance in editor mode (instrument component helper with hook to use inspect and get the call-site; the frame before mesop/components)
  • Send repeated ComponentConfig as part of RenderEvent in editor mode.
  • When clicking on a component instance in canvas; change "focused_component" in EditorService; based on this display in ComponentFieldsEditor the requisite information to modify it.
  • Emit UserEvent with EditorEvent.
  • Implement handlers in Python server for editor event.
  • Handle struct type in editor panel.
  • Allow creating an array element from editor panel.
  • #36

Nice to haves:

  • Create a "plus sign" of new component when hovering
  • Show simplified component tree - in particular, for the parent and descendants of the currently highlighted element

Open questions

  • How do I create a visual placeholder when dropping an item?

Create BUILD alias `colab_deps`

Makes it more obvious this is what you should depend upon.

Potentially include other common deps besides cli that are frequently used.

Send proto diffs from server to client

Diff:

  • Component tree
  • States

Common case:

  • In text_io, it's common to append to the output string and being able to diff even a single field would be helpful.

Consider a composite component API

I'd like to keep composite components as regular Python functions, but then it's difficult to display them in dev tools. In addition, it may be useful way to support content projection for these composite components as this provides a more flexible way of composing components together.

Queue triggering events when processing in-flight events

If a user rapidly triggers events, we currently have a bug.

Instead, Optic should queue the events so that each one is processed at a time.

MVP: Queue on the client.

In the future, we can consider batching events that are fired close together to process them together in the server, this could reduce a network roundtrip, but this could delay sending intermediate results. It's not clear what's the right behavior and it seems probably use case-dependent.

Show an error screen (in prod mode)

In editor mode, we show an error screen which includes stack trace.

For prod, we should show an error screen:

404 - Page not found
500 - Internal Server error

Also, we should log an error in console using console.error but not send the stacktrace from the server to the client.

Docs MVP

  • Overview - what is Optic? Why would you use it? Small code snippet.

Conceptual docs

  • Tutorial: Chat Playground
  • Essential examples

Reference docs

  • Components
  • State
  • Event handling

Use tools to automatically maintain/generate BUILD files

It's quite tedious to maintain the BUILD files and it's easy to make mistakes, particularly with violating strict dependencies by relying on transitive dependencies.

Unfortunately there isn't any one single tool that can call all of Optic's languages, so we can look into some combination of:

  • Gazelle, which supports protos out of the box and Python with an extension - (note: double-check if rules_py has created their own extension)
  • Bzlgen for TS / angular / Sass - I briefly tried using it and because it hasn't been maintained recently, it requires forking it and updating its dependencies like buildozer

Deploy demo site via CI

Right now, I've documented a set of steps to deploy Optic on Google Cloud Run, however it's a bit tedious to do this manually. If we can automate this, then: 1) we can ensure the steps don't get out of date and 2) we can deploy a demo site continuously via CI and either link or embed it from the docs.

If it's not a lot of work, we can do this as part of MVP, otherwise we can defer it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.