crankpy


Namecrankpy JSON
Version 0.2.0 PyPI version JSON
download
home_pageNone
SummaryPython Frontend Framework with Async/Generators, Powered by Crank.js
upload_time2025-10-06 17:51:53
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseNone
keywords crank components pyscript pyodide micropython generators ui hyperscript jsx browser
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # ⚙️🐍 Crank.py

Modern components for Python frontend development.

[![PyScript Compatible](https://img.shields.io/badge/PyScript-Compatible-blue)](https://pyscript.net)
[![Pyodide Compatible](https://img.shields.io/badge/Pyodide-Compatible-green)](https://pyodide.org)
[![MicroPython Compatible](https://img.shields.io/badge/MicroPython-Compatible-orange)](https://micropython.org)
[![MIT License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Built on the [Crank.js](https://crank.js.org/) framework.

## Features

- **Pythonic Hyperscript** - Clean template `h.div["content"]` syntax inspired by JSX
- **Generator Components** - Natural state management using Python generators
- **Async Components** - Components can use `async def`/`await` and `await for`
- **Lifecycle Decorators** - `@ctx.refresh`, `@ctx.after`, `@ctx.cleanup`
- **Dual Runtime** - Full compatibility with both Pyodide and MicroPython runtimes
- **Browser Native** - No build step

## Installation

### PyScript

```html
<py-config type="toml">
packages = ["crankpy"]

[js_modules.main]
"https://esm.run/@b9g/crank@latest/crank.js" = "crank_core"
"https://esm.run/@b9g/crank@latest/dom.js" = "crank_dom"
</py-config>
```

### pip

```bash
pip install crankpy
```

## Quick Start

### Hello World

```python
from crank import h, component
from crank.dom import renderer
from js import document

@component
def Greeting(ctx):
    for _ in ctx:
        yield h.div["Hello, Crank.py!"]

renderer.render(h(Greeting), document.body)
```

### Interactive Counter

```python
@component
def Counter(ctx):
    count = 0

    @ctx.refresh
    def increment():
        nonlocal count
        count += 1

    @ctx.refresh
    def decrement():
        nonlocal count
        count -= 1

    for _ in ctx:
        yield h.div[
            h.h2[f"Count: {count}"],
            h.button(onclick=increment)["+"],
            h.button(onclick=decrement)["-"]
        ]
```

### Props Reassignment

```python
@component
def UserProfile(ctx, props):
    for props in ctx:  # Props automatically update!
        user_id = props.user_id
        user = fetch_user(user_id)  # Fetches when props change

        yield h.div[
            h.img(src=user.avatar),
            h.h2[user.name],
            h.p[user.bio]
        ]

# Usage
h(UserProfile, user_id=123)
```

## Hyperscript Syntax Guide

Crank.py uses a clean, Pythonic hyperscript syntax:

### HTML Elements

```python
# Simple text content
h.div["Hello World"]
h.p["Some text"]

# With properties
h.input(type="text", value=text)
h.div(className="my-class")["Content"]

# Snake_case → kebab-case conversion
h.div(
    data_test_id="button",     # becomes data-test-id
    aria_hidden="true"         # becomes aria-hidden
)["Content"]

# Props spreading (explicit + spread)
h.button(className="btn", **userProps)["Click me"]
h.input(type="text", required=True, **formProps)

# Multiple dict merging (when needed)
h.div(**{**defaults, **themeProps, **userProps})["Content"]

# Nested elements
h.ul[
    h.li["Item 1"],
    h.li["Item 2"],
    h.li[
        "Item with ",
        h.strong["nested"],
        " content"
    ]
]

# Style objects (snake_case → kebab-case)
h.div(style={
    "background_color": "#f0f0f0",  # becomes background-color
    "border_radius": "5px"          # becomes border-radius
})["Styled content"]

# Reserved keywords with spreading
h.div(**{"class": "container", **userProps})["Content"]
# Or better: use className instead of class
h.div(className="container", **userProps)["Content"]
```

### Components

```python
# Component without props
h(MyComponent)

# Component with props
h(MyComponent, name="Alice", count=42)

# Component with children
h(MyComponent)[
    h.p["Child content"]
]

# Component with props and children
h(MyComponent, title="Hello")[
    h.p["Child content"]
]
```

### Fragments

```python
# Simple fragments - just use Python lists!
["Multiple", "children", "without", "wrapper"]
[h.div["Item 1"], h.div["Item 2"]]

# Fragment with props (when you need keys, etc.)
h("", key="my-fragment")["Child 1", "Child 2"]

# In context
h.div[
    h.h1["Title"],
    [h.p["Para 1"], h.p["Para 2"]],  # Simple fragment
    h.footer["Footer"]
]
```

## Component Lifecycle

### Component Signatures

Crank.py supports three component signatures:

```python
# 1. Static components (no state)
@component
def Logo():
    return h.div["🔧 Crank.py"]

# 2. Context-only (internal state)
@component
def Timer(ctx):
    start_time = time.time()
    for _ in ctx:
        elapsed = time.time() - start_time
        yield h.div[f"Time: {elapsed:.1f}s"]

# 3. Context + Props (dynamic)
@component
def TodoItem(ctx, props):
    for props in ctx:  # New props each iteration
        todo = props.todo
        yield h.li[
            h.input(type="checkbox", checked=todo.done),
            h.span[todo.text]
        ]
```

### Lifecycle Decorators

```python
@component
def MyComponent(ctx):
    @ctx.refresh
    def handle_click():
        # Automatically triggers re-render
        pass

    @ctx.schedule
    def schedule_render():
        # Runs before the DOM nodes are inserted
        pass

    @ctx.after
    def after_render(node):
        # Runs after DOM updates
        node.style.color = "blue"

    @ctx.cleanup
    def on_unmount():
        # Cleanup when component unmounts
        clear_interval(timer)

    for _ in ctx:
        yield h.div(onclick=handle_click)["Click me"]
```

## Examples

### Todo App

```python
@component
def TodoApp(ctx):
    todos = []
    new_todo = ""

    @ctx.refresh
    def add_todo():
        nonlocal todos, new_todo
        if new_todo.strip():
            todos.append({"text": new_todo, "done": False})
            new_todo = ""

    @ctx.refresh
    def toggle_todo(index):
        nonlocal todos
        todos[index]["done"] = not todos[index]["done"]

    for _ in ctx:
        yield h.div[
            h.h1["Todo List"],
            h.input(
                type="text",
                value=new_todo,
                oninput=lambda e: setattr(sys.modules[__name__], 'new_todo', e.target.value)
            ),
            h.button(onclick=add_todo)["Add"],
            h.ul[
                [h.li(key=i)[
                    h.input(
                        type="checkbox",
                        checked=todo["done"],
                        onchange=lambda i=i: toggle_todo(i)
                    ),
                    h.span[todo["text"]]
                ] for i, todo in enumerate(todos)]
            ]
        ]
```

### Real-time Clock

```python
@component
def Clock(ctx):
    import asyncio

    async def update_time():
        while True:
            await asyncio.sleep(1)
            ctx.refresh()

    # Start the update loop
    asyncio.create_task(update_time())

    for _ in ctx:
        current_time = time.strftime("%H:%M:%S")
        yield h.div[
            h.strong["Current time: "],
            current_time
        ]
```

## TypeScript-Style Typing

Crank.py provides comprehensive type safety with TypedDict interfaces, Context typing, and full IDE support through Pyright.

### Component Props with TypedDict

Define strict component interfaces using TypedDict:

```python
from typing import TypedDict, Callable, Optional
from crank import component, Context, Props, Children

# Required and optional props
class ButtonProps(TypedDict, total=False):
    onclick: Callable[[], None]  # Event handlers always lowercase
    disabled: bool
    variant: str  # e.g., "primary", "secondary"
    children: Children

# Complex component with nested data
class TodoItemProps(TypedDict):
    todo: "TodoDict"  # Reference to another type
    ontoggle: Callable[[int], None]
    ondelete: Callable[[int], None]
    onedit: Callable[[int, str], None]

class TodoDict(TypedDict):
    id: int
    title: str
    completed: bool

# Type-safe components
@component
def Button(ctx: Context, props: ButtonProps):
    for props in ctx:
        yield h.button(
            onclick=props.get("onclick"),
            disabled=props.get("disabled", False),
            className=f"btn btn-{props.get('variant', 'primary')}"
        )[props.get("children", "Click me")]

@component
def TodoItem(ctx: Context, props: TodoItemProps):
    for props in ctx:
        todo = props["todo"]
        yield h.li[
            h.input(
                type="checkbox",
                checked=todo["completed"],
                onchange=lambda: props["ontoggle"](todo["id"])
            ),
            h.span[todo["title"]],
            h.button(onclick=lambda: props["ondelete"](todo["id"]))["×"]
        ]
```

### Core Crank.py Types

```python
from crank import Element, Context, Props, Children

# Basic types
Props = Dict[str, Any]  # General props dict
Children = Union[str, Element, List["Children"]]  # Nested content

# Generic Context typing (similar to Crank.js)
Context[PropsType, ResultType]  # T = props type, TResult = element result type

# Context with full method typing
def my_component(ctx: Context[MyProps, Element], props: MyProps):
    # All context methods are typed
    ctx.refresh()  # () -> None
    ctx.schedule(callback)  # (Callable) -> None
    ctx.after(callback)    # (Callable) -> None
    ctx.cleanup(callback)  # (Callable) -> None

    # Iterator protocol for generator components
    for props in ctx:  # Each iteration gets updated props (typed as MyProps)
        yield h.div["Updated with new props"]

    # Direct props access with typing
    current_props: MyProps = ctx.props
```

### Component Patterns & Generics

Create reusable, typed component patterns:

```python
from typing import TypedDict, Generic, TypeVar, List

# Generic list component
T = TypeVar('T')

class ListProps(TypedDict, Generic[T]):
    items: List[T]
    render_item: Callable[[T], Element]
    onselect: Callable[[T], None]

@component
def GenericList(ctx: Context[ListProps[T], Element], props: ListProps[T]):
    for props in ctx:  # props is properly typed as ListProps[T]
        yield h.ul[
            [h.li(
                key=i,
                onclick=lambda item=item: props["onselect"](item)
            )[props["render_item"](item)]
             for i, item in enumerate(props["items"])]
        ]

# Usage with type inference
user_list_props: ListProps[User] = {
    "items": users,
    "render_item": lambda user: h.span[user.name],
    "onselect": handle_user_select
}
```

### Advanced Props Patterns

```python
# Union types for polymorphic components
from typing import Union, Literal

class IconButtonProps(TypedDict, total=False):
    variant: Literal["icon", "text", "both"]
    icon: str
    onclick: Callable[[], None]
    children: Children

class FormFieldProps(TypedDict):
    name: str
    value: Union[str, int, bool]
    onchange: Callable[[Union[str, int, bool]], None]
    # Discriminated union based on field type
    field_type: Literal["text", "number", "checkbox"]

@component
def FormField(ctx: Context, props: FormFieldProps):
    for props in ctx:
        field_type = props["field_type"]

        if field_type == "checkbox":
            yield h.input(
                type="checkbox",
                name=props["name"],
                checked=bool(props["value"]),
                onchange=lambda e: props["onchange"](e.target.checked)
            )
        elif field_type == "number":
            yield h.input(
                type="number",
                name=props["name"],
                value=str(props["value"]),
                onchange=lambda e: props["onchange"](int(e.target.value))
            )
        else:  # text
            yield h.input(
                type="text",
                name=props["name"],
                value=str(props["value"]),
                onchange=lambda e: props["onchange"](e.target.value)
            )
```

### Type Checking Setup

Install and configure Pyright for comprehensive type checking:

```bash
# Install type checker
uv add --dev pyright

# Run type checking
uv run pyright crank/

# Run all checks (lint + types)
make check
```

**pyproject.toml configuration:**
```toml
[tool.pyright]
pythonVersion = "3.8"
typeCheckingMode = "basic"
reportUnknownMemberType = false  # For JS interop
reportMissingImports = false     # Ignore PyScript imports
include = ["crank"]
exclude = ["tests", "examples"]
```

### Props as Dictionaries

Components receive props as Python dictionaries (converted from JS objects):

```python
@component
def MyComponent(ctx: Context, props: Props):
    for props in ctx:
        # Access props using dict syntax
        title = props["title"]
        onclick = props["onclick"]

        yield h.div[
            h.h1[title],
            h.button(onclick=onclick)["Click me"]
        ]
```

### Event Props Convention

Use lowercase for all event and callback props:

- `onclick` not `onClick`
- `onchange` not `onChange`
- `ontoggle` not `onToggle`

This matches HTML attribute conventions and provides consistency.

## Testing

Run the test suite:

```bash
# Install dependencies
pip install pytest playwright

# Run tests
pytest tests/
```

## Development

```bash
# Clone the repository
git clone https://github.com/bikeshaving/crankpy.git crankpy
cd crankpy

# Install in development mode
pip install -e ".[dev]"

# Run examples
python -m http.server 8000
# Visit http://localhost:8000/examples/
```

## Why Crank.py?

### Python Web Development, Modernized

Traditional Python web frameworks use templates and server-side rendering. Crank.py brings component-based architecture to Python:

- **Reusable Components** - Build UIs from composable pieces
- **Dynamic Updates** - Explicit re-rendering with ctx.refresh()
- **Generator-Powered** - Natural state management with Python generators
- **Browser-Native** - Run Python directly in the browser via PyScript

### Perfect for:

- **PyScript Applications** - Rich client-side Python apps
- **Educational Projects** - Teaching web development with Python
- **Prototyping** - Rapid UI development without JavaScript
- **Data Visualization** - Interactive Python data apps in the browser

## Advanced Features

### Refs - Direct DOM Access

Use `ref` callbacks to access rendered DOM elements directly:

```python
@component
def VideoPlayer(ctx):
    video_element = None

    def set_video_ref(el):
        nonlocal video_element
        video_element = el

    @ctx.refresh
    def play():
        if video_element:
            video_element.play()

    @ctx.refresh
    def pause():
        if video_element:
            video_element.pause()

    for _ in ctx:
        yield h.div[
            h.video(
                src="/path/to/video.mp4",
                ref=set_video_ref
            ),
            h.button(onclick=play)["Play"],
            h.button(onclick=pause)["Pause"]
        ]
```

**Ref Patterns:**
- Refs fire once when elements are first rendered
- Don't work on fragments - use on host elements only
- For components, explicitly pass `ref` to child elements
- Useful for focus management, DOM measurements, third-party integrations

```python
@component
def AutoFocusInput(ctx, props):
    for props in ctx:
        yield h.input(
            type="text",
            placeholder=props.get("placeholder", ""),
            ref=lambda el: el.focus()  # Auto-focus when rendered
        )
```

### Fragments - Multiple Children Without Wrappers

Fragments let you return multiple elements without extra DOM nodes:

```python
# Simple fragments - just use Python lists!
@component
def UserInfo(ctx, props):
    user = props["user"]
    for props in ctx:
        yield [
            h.h2[user["name"]],
            h.p[user["bio"]],
            h.span[f"Joined: {user['joined']}"]
        ]

# Fragment with props (for keys, etc.)
@component
def ConditionalContent(ctx, props):
    show_content = props.get("show", False)
    for props in ctx:
        if show_content:
            yield h("", key="content-fragment")[
                h.div["Content block 1"],
                h.div["Content block 2"]
            ]
        else:
            yield h("", key="empty-fragment")["No content"]

# Mixed fragments in JSX-like syntax
@component
def Navigation(ctx):
    for _ in ctx:
        yield h.nav[
            h.div(className="logo")["MyApp"],
            [  # Fragment for nav items
                h.a(href="/home")["Home"],
                h.a(href="/about")["About"],
                h.a(href="/contact")["Contact"]
            ],
            h.button["Menu"]
        ]
```

### Key Prop - List Reconciliation

Keys help Crank identify which elements have changed in lists:

```python
@component
def TodoList(ctx, props):
    for props in ctx:
        todos = props["todos"]
        yield h.ul[
            [h.li(key=todo["id"])[
                h.input(
                    type="checkbox",
                    checked=todo["completed"],
                    onchange=lambda todo_id=todo["id"]: props["onToggle"](todo_id)
                ),
                h.span[todo["text"]],
                h.button(onclick=lambda todo_id=todo["id"]: props["onDelete"](todo_id))["×"]
            ] for todo in todos]
        ]

# Without keys - elements match by position (can cause issues)
# With keys - elements match by identity (preserves state correctly)

@component
def DynamicList(ctx):
    items = ["A", "B", "C", "D"]
    reversed_items = False

    @ctx.refresh
    def toggle_order():
        nonlocal reversed_items
        reversed_items = not reversed_items

    for _ in ctx:
        current_items = items[::-1] if reversed_items else items
        yield h.div[
            h.button(onclick=toggle_order)["Toggle Order"],
            h.ul[
                [h.li(key=item)[
                    f"Item {item} (with preserved state)"
                ] for item in current_items]
            ]
        ]
```

**Key Guidelines:**
- Use stable, unique values (IDs, not array indices)
- Keys only need to be unique among siblings
- Can be strings, numbers, or any JavaScript value
- Essential for stateful components and form inputs

### Copy Prop - Prevent Re-rendering

The `copy` prop prevents elements from re-rendering for performance optimization:

```python
@component
def ExpensiveList(ctx, props):
    for props in ctx:
        items = props["items"]
        yield h.ul[
            [h.li(
                key=item["id"],
                copy=not item.get("hasChanged", True)  # Skip render if unchanged
            )[
                h(ExpensiveComponent, data=item["data"])
            ] for item in items]
        ]

# Copy with string selectors (Crank 0.7+)
@component
def SmartForm(ctx, props):
    for props in ctx:
        yield h.form[
            # Copy all props except value (keeps input uncontrolled)
            h.input(
                copy="!value",
                type="text",
                placeholder="Enter text...",
                name="username"
            ),

            # Copy only specific props
            h.div(
                copy="class id",
                className="form-section",
                id="user-info",
                data_updated=props.get("timestamp")
            )[
                h.label["Username"]
            ],

            # Copy children from previous render
            h.div(copy="children", className="dynamic")[
                # Children preserved from last render
            ]
        ]
```

**Copy Prop Syntax:**
- `copy=True` - Prevent all re-rendering
- `copy=False` - Normal re-rendering (default)
- `copy="!value"` - Copy all props except `value`
- `copy="class children"` - Copy only `class` and `children`
- Cannot mix `!` and regular syntax

### Special Components

#### Raw - Inject HTML/DOM Nodes

```python
@component
def MarkdownRenderer(ctx, props):
    for props in ctx:
        # Process markdown to HTML
        markdown_text = props["markdown"]
        html_content = markdown_to_html(markdown_text)  # Your markdown processor

        yield h.div[
            h(Raw, value=html_content)
        ]

# Insert actual DOM nodes
@component
def CanvasChart(ctx, props):
    for props in ctx:
        # Create chart canvas with external library
        canvas_node = create_chart(props["data"])

        yield h.div[
            h.h3["Sales Chart"],
            h(Raw, value=canvas_node)
        ]
```

#### Portal - Render Into Different DOM Location

```python
from js import document

@component
def Modal(ctx, props):
    for props in ctx:
        is_open = props.get("isOpen", False)
        if is_open:
            # Render modal into document body instead of current location
            modal_root = document.getElementById("modal-root")
            yield h(Portal, root=modal_root)[
                h.div(className="modal-backdrop", onclick=props["onClose"])[
                    h.div(className="modal-content", onclick=lambda e: e.stopPropagation())[
                        h.div(className="modal-header")[
                            h.h2[props["title"]],
                            h.button(onclick=props["onClose"])["×"]
                        ],
                        h.div(className="modal-body")[
                            props.get("children", [])
                        ]
                    ]
                ]
            ]

# Usage
@component
def App(ctx):
    show_modal = False

    @ctx.refresh
    def open_modal():
        nonlocal show_modal
        show_modal = True

    @ctx.refresh
    def close_modal():
        nonlocal show_modal
        show_modal = False

    for _ in ctx:
        yield h.div[
            h.h1["My App"],
            h.button(onclick=open_modal)["Open Modal"],
            h(Modal,
                isOpen=show_modal,
                title="Example Modal",
                onClose=close_modal
            )["Modal content here!"]
        ]
```

#### Text - Explicit Text Node Control

```python
@component
def TextManipulator(ctx):
    text_node = None

    def set_text_ref(el):
        nonlocal text_node
        text_node = el

    @ctx.refresh
    def update_text():
        if text_node:
            text_node.textContent = "Updated directly!"

    for _ in ctx:
        yield h.div[
            h(Text, value="Original text", ref=set_text_ref),
            h.button(onclick=update_text)["Update Text"]
        ]

# Multiple separate text nodes (no concatenation)
@component
def FormattedText(ctx, props):
    for props in ctx:
        yield h.p[
            h(Text, value="Hello "),
            h(Text, value=props["name"]),
            h(Text, value="!")
        ]  # Creates 3 separate Text nodes
```

#### Copy - Prevent Subtree Re-rendering

```python
@component
def MemoizedComponent(ctx, props):
    last_props = None

    for props in ctx:
        if last_props and props_equal(props, last_props):
            # Don't re-render if props haven't changed
            yield h(Copy)
        else:
            yield h(ExpensiveComponent, **props)
        last_props = props

def props_equal(a, b):
    """Shallow comparison of props"""
    return (
        set(a.keys()) == set(b.keys()) and
        all(a[key] == b[key] for key in a.keys())
    )

# Higher-order memo component
def memo(Component):
    @component
    def MemoWrapper(ctx, props):
        last_props = None
        yield h(Component, **props)

        for props in ctx:
            if last_props and props_equal(props, last_props):
                yield h(Copy)
            else:
                yield h(Component, **props)
            last_props = props

    return MemoWrapper

# Usage
@memo
@component
def ExpensiveItem(ctx, props):
    for props in ctx:
        # Expensive computation here
        yield h.div[f"Processed: {props['data']}"]
```

### Performance Patterns

```python
# Combining keys, copy, and memoization
@component
def OptimizedList(ctx, props):
    for props in ctx:
        items = props["items"]
        yield h.ul[
            [h.li(
                key=item["id"],
                copy=not item.get("_dirty", False)  # Skip clean items
            )[
                h(MemoizedItem,
                    data=item["data"],
                    onUpdate=props["onItemUpdate"]
                )
            ] for item in items]
        ]

# Selective prop copying for performance
@component
def SmartComponent(ctx, props):
    for props in ctx:
        yield h.div[
            # Only re-render when content changes, preserve styling
            h.div(copy="class style")[
                props["dynamicContent"]
            ],

            # Expensive chart that rarely changes
            h.div(copy=not props.get("chartDataChanged", False))[
                h(ChartComponent, data=props["chartData"])
            ]
        ]
```

## Learn More

- **[Crank.js Documentation](https://crank.js.org/)** - The underlying framework
- **[PyScript Guide](https://pyscript.net/)** - Running Python in browsers
- **[Examples](examples/)** - See Crank.py in action

## Contributing

Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md) first.

## License
MIT © 2025

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "crankpy",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": "Brian Kim <briankimpossible@gmail.com>",
    "keywords": "crank, components, pyscript, pyodide, micropython, generators, ui, hyperscript, jsx, browser",
    "author": null,
    "author_email": "Brian Kim <briankimpossible@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/73/7a/a966fd3ae55ffdb7d5afb009be7fba6aa76840f973932b1f03ec67d478a5/crankpy-0.2.0.tar.gz",
    "platform": null,
    "description": "# \u2699\ufe0f\ud83d\udc0d Crank.py\n\nModern components for Python frontend development.\n\n[![PyScript Compatible](https://img.shields.io/badge/PyScript-Compatible-blue)](https://pyscript.net)\n[![Pyodide Compatible](https://img.shields.io/badge/Pyodide-Compatible-green)](https://pyodide.org)\n[![MicroPython Compatible](https://img.shields.io/badge/MicroPython-Compatible-orange)](https://micropython.org)\n[![MIT License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n\nBuilt on the [Crank.js](https://crank.js.org/) framework.\n\n## Features\n\n- **Pythonic Hyperscript** - Clean template `h.div[\"content\"]` syntax inspired by JSX\n- **Generator Components** - Natural state management using Python generators\n- **Async Components** - Components can use `async def`/`await` and `await for`\n- **Lifecycle Decorators** - `@ctx.refresh`, `@ctx.after`, `@ctx.cleanup`\n- **Dual Runtime** - Full compatibility with both Pyodide and MicroPython runtimes\n- **Browser Native** - No build step\n\n## Installation\n\n### PyScript\n\n```html\n<py-config type=\"toml\">\npackages = [\"crankpy\"]\n\n[js_modules.main]\n\"https://esm.run/@b9g/crank@latest/crank.js\" = \"crank_core\"\n\"https://esm.run/@b9g/crank@latest/dom.js\" = \"crank_dom\"\n</py-config>\n```\n\n### pip\n\n```bash\npip install crankpy\n```\n\n## Quick Start\n\n### Hello World\n\n```python\nfrom crank import h, component\nfrom crank.dom import renderer\nfrom js import document\n\n@component\ndef Greeting(ctx):\n    for _ in ctx:\n        yield h.div[\"Hello, Crank.py!\"]\n\nrenderer.render(h(Greeting), document.body)\n```\n\n### Interactive Counter\n\n```python\n@component\ndef Counter(ctx):\n    count = 0\n\n    @ctx.refresh\n    def increment():\n        nonlocal count\n        count += 1\n\n    @ctx.refresh\n    def decrement():\n        nonlocal count\n        count -= 1\n\n    for _ in ctx:\n        yield h.div[\n            h.h2[f\"Count: {count}\"],\n            h.button(onclick=increment)[\"+\"],\n            h.button(onclick=decrement)[\"-\"]\n        ]\n```\n\n### Props Reassignment\n\n```python\n@component\ndef UserProfile(ctx, props):\n    for props in ctx:  # Props automatically update!\n        user_id = props.user_id\n        user = fetch_user(user_id)  # Fetches when props change\n\n        yield h.div[\n            h.img(src=user.avatar),\n            h.h2[user.name],\n            h.p[user.bio]\n        ]\n\n# Usage\nh(UserProfile, user_id=123)\n```\n\n## Hyperscript Syntax Guide\n\nCrank.py uses a clean, Pythonic hyperscript syntax:\n\n### HTML Elements\n\n```python\n# Simple text content\nh.div[\"Hello World\"]\nh.p[\"Some text\"]\n\n# With properties\nh.input(type=\"text\", value=text)\nh.div(className=\"my-class\")[\"Content\"]\n\n# Snake_case \u2192 kebab-case conversion\nh.div(\n    data_test_id=\"button\",     # becomes data-test-id\n    aria_hidden=\"true\"         # becomes aria-hidden\n)[\"Content\"]\n\n# Props spreading (explicit + spread)\nh.button(className=\"btn\", **userProps)[\"Click me\"]\nh.input(type=\"text\", required=True, **formProps)\n\n# Multiple dict merging (when needed)\nh.div(**{**defaults, **themeProps, **userProps})[\"Content\"]\n\n# Nested elements\nh.ul[\n    h.li[\"Item 1\"],\n    h.li[\"Item 2\"],\n    h.li[\n        \"Item with \",\n        h.strong[\"nested\"],\n        \" content\"\n    ]\n]\n\n# Style objects (snake_case \u2192 kebab-case)\nh.div(style={\n    \"background_color\": \"#f0f0f0\",  # becomes background-color\n    \"border_radius\": \"5px\"          # becomes border-radius\n})[\"Styled content\"]\n\n# Reserved keywords with spreading\nh.div(**{\"class\": \"container\", **userProps})[\"Content\"]\n# Or better: use className instead of class\nh.div(className=\"container\", **userProps)[\"Content\"]\n```\n\n### Components\n\n```python\n# Component without props\nh(MyComponent)\n\n# Component with props\nh(MyComponent, name=\"Alice\", count=42)\n\n# Component with children\nh(MyComponent)[\n    h.p[\"Child content\"]\n]\n\n# Component with props and children\nh(MyComponent, title=\"Hello\")[\n    h.p[\"Child content\"]\n]\n```\n\n### Fragments\n\n```python\n# Simple fragments - just use Python lists!\n[\"Multiple\", \"children\", \"without\", \"wrapper\"]\n[h.div[\"Item 1\"], h.div[\"Item 2\"]]\n\n# Fragment with props (when you need keys, etc.)\nh(\"\", key=\"my-fragment\")[\"Child 1\", \"Child 2\"]\n\n# In context\nh.div[\n    h.h1[\"Title\"],\n    [h.p[\"Para 1\"], h.p[\"Para 2\"]],  # Simple fragment\n    h.footer[\"Footer\"]\n]\n```\n\n## Component Lifecycle\n\n### Component Signatures\n\nCrank.py supports three component signatures:\n\n```python\n# 1. Static components (no state)\n@component\ndef Logo():\n    return h.div[\"\ud83d\udd27 Crank.py\"]\n\n# 2. Context-only (internal state)\n@component\ndef Timer(ctx):\n    start_time = time.time()\n    for _ in ctx:\n        elapsed = time.time() - start_time\n        yield h.div[f\"Time: {elapsed:.1f}s\"]\n\n# 3. Context + Props (dynamic)\n@component\ndef TodoItem(ctx, props):\n    for props in ctx:  # New props each iteration\n        todo = props.todo\n        yield h.li[\n            h.input(type=\"checkbox\", checked=todo.done),\n            h.span[todo.text]\n        ]\n```\n\n### Lifecycle Decorators\n\n```python\n@component\ndef MyComponent(ctx):\n    @ctx.refresh\n    def handle_click():\n        # Automatically triggers re-render\n        pass\n\n    @ctx.schedule\n    def schedule_render():\n        # Runs before the DOM nodes are inserted\n        pass\n\n    @ctx.after\n    def after_render(node):\n        # Runs after DOM updates\n        node.style.color = \"blue\"\n\n    @ctx.cleanup\n    def on_unmount():\n        # Cleanup when component unmounts\n        clear_interval(timer)\n\n    for _ in ctx:\n        yield h.div(onclick=handle_click)[\"Click me\"]\n```\n\n## Examples\n\n### Todo App\n\n```python\n@component\ndef TodoApp(ctx):\n    todos = []\n    new_todo = \"\"\n\n    @ctx.refresh\n    def add_todo():\n        nonlocal todos, new_todo\n        if new_todo.strip():\n            todos.append({\"text\": new_todo, \"done\": False})\n            new_todo = \"\"\n\n    @ctx.refresh\n    def toggle_todo(index):\n        nonlocal todos\n        todos[index][\"done\"] = not todos[index][\"done\"]\n\n    for _ in ctx:\n        yield h.div[\n            h.h1[\"Todo List\"],\n            h.input(\n                type=\"text\",\n                value=new_todo,\n                oninput=lambda e: setattr(sys.modules[__name__], 'new_todo', e.target.value)\n            ),\n            h.button(onclick=add_todo)[\"Add\"],\n            h.ul[\n                [h.li(key=i)[\n                    h.input(\n                        type=\"checkbox\",\n                        checked=todo[\"done\"],\n                        onchange=lambda i=i: toggle_todo(i)\n                    ),\n                    h.span[todo[\"text\"]]\n                ] for i, todo in enumerate(todos)]\n            ]\n        ]\n```\n\n### Real-time Clock\n\n```python\n@component\ndef Clock(ctx):\n    import asyncio\n\n    async def update_time():\n        while True:\n            await asyncio.sleep(1)\n            ctx.refresh()\n\n    # Start the update loop\n    asyncio.create_task(update_time())\n\n    for _ in ctx:\n        current_time = time.strftime(\"%H:%M:%S\")\n        yield h.div[\n            h.strong[\"Current time: \"],\n            current_time\n        ]\n```\n\n## TypeScript-Style Typing\n\nCrank.py provides comprehensive type safety with TypedDict interfaces, Context typing, and full IDE support through Pyright.\n\n### Component Props with TypedDict\n\nDefine strict component interfaces using TypedDict:\n\n```python\nfrom typing import TypedDict, Callable, Optional\nfrom crank import component, Context, Props, Children\n\n# Required and optional props\nclass ButtonProps(TypedDict, total=False):\n    onclick: Callable[[], None]  # Event handlers always lowercase\n    disabled: bool\n    variant: str  # e.g., \"primary\", \"secondary\"\n    children: Children\n\n# Complex component with nested data\nclass TodoItemProps(TypedDict):\n    todo: \"TodoDict\"  # Reference to another type\n    ontoggle: Callable[[int], None]\n    ondelete: Callable[[int], None]\n    onedit: Callable[[int, str], None]\n\nclass TodoDict(TypedDict):\n    id: int\n    title: str\n    completed: bool\n\n# Type-safe components\n@component\ndef Button(ctx: Context, props: ButtonProps):\n    for props in ctx:\n        yield h.button(\n            onclick=props.get(\"onclick\"),\n            disabled=props.get(\"disabled\", False),\n            className=f\"btn btn-{props.get('variant', 'primary')}\"\n        )[props.get(\"children\", \"Click me\")]\n\n@component\ndef TodoItem(ctx: Context, props: TodoItemProps):\n    for props in ctx:\n        todo = props[\"todo\"]\n        yield h.li[\n            h.input(\n                type=\"checkbox\",\n                checked=todo[\"completed\"],\n                onchange=lambda: props[\"ontoggle\"](todo[\"id\"])\n            ),\n            h.span[todo[\"title\"]],\n            h.button(onclick=lambda: props[\"ondelete\"](todo[\"id\"]))[\"\u00d7\"]\n        ]\n```\n\n### Core Crank.py Types\n\n```python\nfrom crank import Element, Context, Props, Children\n\n# Basic types\nProps = Dict[str, Any]  # General props dict\nChildren = Union[str, Element, List[\"Children\"]]  # Nested content\n\n# Generic Context typing (similar to Crank.js)\nContext[PropsType, ResultType]  # T = props type, TResult = element result type\n\n# Context with full method typing\ndef my_component(ctx: Context[MyProps, Element], props: MyProps):\n    # All context methods are typed\n    ctx.refresh()  # () -> None\n    ctx.schedule(callback)  # (Callable) -> None\n    ctx.after(callback)    # (Callable) -> None\n    ctx.cleanup(callback)  # (Callable) -> None\n\n    # Iterator protocol for generator components\n    for props in ctx:  # Each iteration gets updated props (typed as MyProps)\n        yield h.div[\"Updated with new props\"]\n\n    # Direct props access with typing\n    current_props: MyProps = ctx.props\n```\n\n### Component Patterns & Generics\n\nCreate reusable, typed component patterns:\n\n```python\nfrom typing import TypedDict, Generic, TypeVar, List\n\n# Generic list component\nT = TypeVar('T')\n\nclass ListProps(TypedDict, Generic[T]):\n    items: List[T]\n    render_item: Callable[[T], Element]\n    onselect: Callable[[T], None]\n\n@component\ndef GenericList(ctx: Context[ListProps[T], Element], props: ListProps[T]):\n    for props in ctx:  # props is properly typed as ListProps[T]\n        yield h.ul[\n            [h.li(\n                key=i,\n                onclick=lambda item=item: props[\"onselect\"](item)\n            )[props[\"render_item\"](item)]\n             for i, item in enumerate(props[\"items\"])]\n        ]\n\n# Usage with type inference\nuser_list_props: ListProps[User] = {\n    \"items\": users,\n    \"render_item\": lambda user: h.span[user.name],\n    \"onselect\": handle_user_select\n}\n```\n\n### Advanced Props Patterns\n\n```python\n# Union types for polymorphic components\nfrom typing import Union, Literal\n\nclass IconButtonProps(TypedDict, total=False):\n    variant: Literal[\"icon\", \"text\", \"both\"]\n    icon: str\n    onclick: Callable[[], None]\n    children: Children\n\nclass FormFieldProps(TypedDict):\n    name: str\n    value: Union[str, int, bool]\n    onchange: Callable[[Union[str, int, bool]], None]\n    # Discriminated union based on field type\n    field_type: Literal[\"text\", \"number\", \"checkbox\"]\n\n@component\ndef FormField(ctx: Context, props: FormFieldProps):\n    for props in ctx:\n        field_type = props[\"field_type\"]\n\n        if field_type == \"checkbox\":\n            yield h.input(\n                type=\"checkbox\",\n                name=props[\"name\"],\n                checked=bool(props[\"value\"]),\n                onchange=lambda e: props[\"onchange\"](e.target.checked)\n            )\n        elif field_type == \"number\":\n            yield h.input(\n                type=\"number\",\n                name=props[\"name\"],\n                value=str(props[\"value\"]),\n                onchange=lambda e: props[\"onchange\"](int(e.target.value))\n            )\n        else:  # text\n            yield h.input(\n                type=\"text\",\n                name=props[\"name\"],\n                value=str(props[\"value\"]),\n                onchange=lambda e: props[\"onchange\"](e.target.value)\n            )\n```\n\n### Type Checking Setup\n\nInstall and configure Pyright for comprehensive type checking:\n\n```bash\n# Install type checker\nuv add --dev pyright\n\n# Run type checking\nuv run pyright crank/\n\n# Run all checks (lint + types)\nmake check\n```\n\n**pyproject.toml configuration:**\n```toml\n[tool.pyright]\npythonVersion = \"3.8\"\ntypeCheckingMode = \"basic\"\nreportUnknownMemberType = false  # For JS interop\nreportMissingImports = false     # Ignore PyScript imports\ninclude = [\"crank\"]\nexclude = [\"tests\", \"examples\"]\n```\n\n### Props as Dictionaries\n\nComponents receive props as Python dictionaries (converted from JS objects):\n\n```python\n@component\ndef MyComponent(ctx: Context, props: Props):\n    for props in ctx:\n        # Access props using dict syntax\n        title = props[\"title\"]\n        onclick = props[\"onclick\"]\n\n        yield h.div[\n            h.h1[title],\n            h.button(onclick=onclick)[\"Click me\"]\n        ]\n```\n\n### Event Props Convention\n\nUse lowercase for all event and callback props:\n\n- `onclick` not `onClick`\n- `onchange` not `onChange`\n- `ontoggle` not `onToggle`\n\nThis matches HTML attribute conventions and provides consistency.\n\n## Testing\n\nRun the test suite:\n\n```bash\n# Install dependencies\npip install pytest playwright\n\n# Run tests\npytest tests/\n```\n\n## Development\n\n```bash\n# Clone the repository\ngit clone https://github.com/bikeshaving/crankpy.git crankpy\ncd crankpy\n\n# Install in development mode\npip install -e \".[dev]\"\n\n# Run examples\npython -m http.server 8000\n# Visit http://localhost:8000/examples/\n```\n\n## Why Crank.py?\n\n### Python Web Development, Modernized\n\nTraditional Python web frameworks use templates and server-side rendering. Crank.py brings component-based architecture to Python:\n\n- **Reusable Components** - Build UIs from composable pieces\n- **Dynamic Updates** - Explicit re-rendering with ctx.refresh()\n- **Generator-Powered** - Natural state management with Python generators\n- **Browser-Native** - Run Python directly in the browser via PyScript\n\n### Perfect for:\n\n- **PyScript Applications** - Rich client-side Python apps\n- **Educational Projects** - Teaching web development with Python\n- **Prototyping** - Rapid UI development without JavaScript\n- **Data Visualization** - Interactive Python data apps in the browser\n\n## Advanced Features\n\n### Refs - Direct DOM Access\n\nUse `ref` callbacks to access rendered DOM elements directly:\n\n```python\n@component\ndef VideoPlayer(ctx):\n    video_element = None\n\n    def set_video_ref(el):\n        nonlocal video_element\n        video_element = el\n\n    @ctx.refresh\n    def play():\n        if video_element:\n            video_element.play()\n\n    @ctx.refresh\n    def pause():\n        if video_element:\n            video_element.pause()\n\n    for _ in ctx:\n        yield h.div[\n            h.video(\n                src=\"/path/to/video.mp4\",\n                ref=set_video_ref\n            ),\n            h.button(onclick=play)[\"Play\"],\n            h.button(onclick=pause)[\"Pause\"]\n        ]\n```\n\n**Ref Patterns:**\n- Refs fire once when elements are first rendered\n- Don't work on fragments - use on host elements only\n- For components, explicitly pass `ref` to child elements\n- Useful for focus management, DOM measurements, third-party integrations\n\n```python\n@component\ndef AutoFocusInput(ctx, props):\n    for props in ctx:\n        yield h.input(\n            type=\"text\",\n            placeholder=props.get(\"placeholder\", \"\"),\n            ref=lambda el: el.focus()  # Auto-focus when rendered\n        )\n```\n\n### Fragments - Multiple Children Without Wrappers\n\nFragments let you return multiple elements without extra DOM nodes:\n\n```python\n# Simple fragments - just use Python lists!\n@component\ndef UserInfo(ctx, props):\n    user = props[\"user\"]\n    for props in ctx:\n        yield [\n            h.h2[user[\"name\"]],\n            h.p[user[\"bio\"]],\n            h.span[f\"Joined: {user['joined']}\"]\n        ]\n\n# Fragment with props (for keys, etc.)\n@component\ndef ConditionalContent(ctx, props):\n    show_content = props.get(\"show\", False)\n    for props in ctx:\n        if show_content:\n            yield h(\"\", key=\"content-fragment\")[\n                h.div[\"Content block 1\"],\n                h.div[\"Content block 2\"]\n            ]\n        else:\n            yield h(\"\", key=\"empty-fragment\")[\"No content\"]\n\n# Mixed fragments in JSX-like syntax\n@component\ndef Navigation(ctx):\n    for _ in ctx:\n        yield h.nav[\n            h.div(className=\"logo\")[\"MyApp\"],\n            [  # Fragment for nav items\n                h.a(href=\"/home\")[\"Home\"],\n                h.a(href=\"/about\")[\"About\"],\n                h.a(href=\"/contact\")[\"Contact\"]\n            ],\n            h.button[\"Menu\"]\n        ]\n```\n\n### Key Prop - List Reconciliation\n\nKeys help Crank identify which elements have changed in lists:\n\n```python\n@component\ndef TodoList(ctx, props):\n    for props in ctx:\n        todos = props[\"todos\"]\n        yield h.ul[\n            [h.li(key=todo[\"id\"])[\n                h.input(\n                    type=\"checkbox\",\n                    checked=todo[\"completed\"],\n                    onchange=lambda todo_id=todo[\"id\"]: props[\"onToggle\"](todo_id)\n                ),\n                h.span[todo[\"text\"]],\n                h.button(onclick=lambda todo_id=todo[\"id\"]: props[\"onDelete\"](todo_id))[\"\u00d7\"]\n            ] for todo in todos]\n        ]\n\n# Without keys - elements match by position (can cause issues)\n# With keys - elements match by identity (preserves state correctly)\n\n@component\ndef DynamicList(ctx):\n    items = [\"A\", \"B\", \"C\", \"D\"]\n    reversed_items = False\n\n    @ctx.refresh\n    def toggle_order():\n        nonlocal reversed_items\n        reversed_items = not reversed_items\n\n    for _ in ctx:\n        current_items = items[::-1] if reversed_items else items\n        yield h.div[\n            h.button(onclick=toggle_order)[\"Toggle Order\"],\n            h.ul[\n                [h.li(key=item)[\n                    f\"Item {item} (with preserved state)\"\n                ] for item in current_items]\n            ]\n        ]\n```\n\n**Key Guidelines:**\n- Use stable, unique values (IDs, not array indices)\n- Keys only need to be unique among siblings\n- Can be strings, numbers, or any JavaScript value\n- Essential for stateful components and form inputs\n\n### Copy Prop - Prevent Re-rendering\n\nThe `copy` prop prevents elements from re-rendering for performance optimization:\n\n```python\n@component\ndef ExpensiveList(ctx, props):\n    for props in ctx:\n        items = props[\"items\"]\n        yield h.ul[\n            [h.li(\n                key=item[\"id\"],\n                copy=not item.get(\"hasChanged\", True)  # Skip render if unchanged\n            )[\n                h(ExpensiveComponent, data=item[\"data\"])\n            ] for item in items]\n        ]\n\n# Copy with string selectors (Crank 0.7+)\n@component\ndef SmartForm(ctx, props):\n    for props in ctx:\n        yield h.form[\n            # Copy all props except value (keeps input uncontrolled)\n            h.input(\n                copy=\"!value\",\n                type=\"text\",\n                placeholder=\"Enter text...\",\n                name=\"username\"\n            ),\n\n            # Copy only specific props\n            h.div(\n                copy=\"class id\",\n                className=\"form-section\",\n                id=\"user-info\",\n                data_updated=props.get(\"timestamp\")\n            )[\n                h.label[\"Username\"]\n            ],\n\n            # Copy children from previous render\n            h.div(copy=\"children\", className=\"dynamic\")[\n                # Children preserved from last render\n            ]\n        ]\n```\n\n**Copy Prop Syntax:**\n- `copy=True` - Prevent all re-rendering\n- `copy=False` - Normal re-rendering (default)\n- `copy=\"!value\"` - Copy all props except `value`\n- `copy=\"class children\"` - Copy only `class` and `children`\n- Cannot mix `!` and regular syntax\n\n### Special Components\n\n#### Raw - Inject HTML/DOM Nodes\n\n```python\n@component\ndef MarkdownRenderer(ctx, props):\n    for props in ctx:\n        # Process markdown to HTML\n        markdown_text = props[\"markdown\"]\n        html_content = markdown_to_html(markdown_text)  # Your markdown processor\n\n        yield h.div[\n            h(Raw, value=html_content)\n        ]\n\n# Insert actual DOM nodes\n@component\ndef CanvasChart(ctx, props):\n    for props in ctx:\n        # Create chart canvas with external library\n        canvas_node = create_chart(props[\"data\"])\n\n        yield h.div[\n            h.h3[\"Sales Chart\"],\n            h(Raw, value=canvas_node)\n        ]\n```\n\n#### Portal - Render Into Different DOM Location\n\n```python\nfrom js import document\n\n@component\ndef Modal(ctx, props):\n    for props in ctx:\n        is_open = props.get(\"isOpen\", False)\n        if is_open:\n            # Render modal into document body instead of current location\n            modal_root = document.getElementById(\"modal-root\")\n            yield h(Portal, root=modal_root)[\n                h.div(className=\"modal-backdrop\", onclick=props[\"onClose\"])[\n                    h.div(className=\"modal-content\", onclick=lambda e: e.stopPropagation())[\n                        h.div(className=\"modal-header\")[\n                            h.h2[props[\"title\"]],\n                            h.button(onclick=props[\"onClose\"])[\"\u00d7\"]\n                        ],\n                        h.div(className=\"modal-body\")[\n                            props.get(\"children\", [])\n                        ]\n                    ]\n                ]\n            ]\n\n# Usage\n@component\ndef App(ctx):\n    show_modal = False\n\n    @ctx.refresh\n    def open_modal():\n        nonlocal show_modal\n        show_modal = True\n\n    @ctx.refresh\n    def close_modal():\n        nonlocal show_modal\n        show_modal = False\n\n    for _ in ctx:\n        yield h.div[\n            h.h1[\"My App\"],\n            h.button(onclick=open_modal)[\"Open Modal\"],\n            h(Modal,\n                isOpen=show_modal,\n                title=\"Example Modal\",\n                onClose=close_modal\n            )[\"Modal content here!\"]\n        ]\n```\n\n#### Text - Explicit Text Node Control\n\n```python\n@component\ndef TextManipulator(ctx):\n    text_node = None\n\n    def set_text_ref(el):\n        nonlocal text_node\n        text_node = el\n\n    @ctx.refresh\n    def update_text():\n        if text_node:\n            text_node.textContent = \"Updated directly!\"\n\n    for _ in ctx:\n        yield h.div[\n            h(Text, value=\"Original text\", ref=set_text_ref),\n            h.button(onclick=update_text)[\"Update Text\"]\n        ]\n\n# Multiple separate text nodes (no concatenation)\n@component\ndef FormattedText(ctx, props):\n    for props in ctx:\n        yield h.p[\n            h(Text, value=\"Hello \"),\n            h(Text, value=props[\"name\"]),\n            h(Text, value=\"!\")\n        ]  # Creates 3 separate Text nodes\n```\n\n#### Copy - Prevent Subtree Re-rendering\n\n```python\n@component\ndef MemoizedComponent(ctx, props):\n    last_props = None\n\n    for props in ctx:\n        if last_props and props_equal(props, last_props):\n            # Don't re-render if props haven't changed\n            yield h(Copy)\n        else:\n            yield h(ExpensiveComponent, **props)\n        last_props = props\n\ndef props_equal(a, b):\n    \"\"\"Shallow comparison of props\"\"\"\n    return (\n        set(a.keys()) == set(b.keys()) and\n        all(a[key] == b[key] for key in a.keys())\n    )\n\n# Higher-order memo component\ndef memo(Component):\n    @component\n    def MemoWrapper(ctx, props):\n        last_props = None\n        yield h(Component, **props)\n\n        for props in ctx:\n            if last_props and props_equal(props, last_props):\n                yield h(Copy)\n            else:\n                yield h(Component, **props)\n            last_props = props\n\n    return MemoWrapper\n\n# Usage\n@memo\n@component\ndef ExpensiveItem(ctx, props):\n    for props in ctx:\n        # Expensive computation here\n        yield h.div[f\"Processed: {props['data']}\"]\n```\n\n### Performance Patterns\n\n```python\n# Combining keys, copy, and memoization\n@component\ndef OptimizedList(ctx, props):\n    for props in ctx:\n        items = props[\"items\"]\n        yield h.ul[\n            [h.li(\n                key=item[\"id\"],\n                copy=not item.get(\"_dirty\", False)  # Skip clean items\n            )[\n                h(MemoizedItem,\n                    data=item[\"data\"],\n                    onUpdate=props[\"onItemUpdate\"]\n                )\n            ] for item in items]\n        ]\n\n# Selective prop copying for performance\n@component\ndef SmartComponent(ctx, props):\n    for props in ctx:\n        yield h.div[\n            # Only re-render when content changes, preserve styling\n            h.div(copy=\"class style\")[\n                props[\"dynamicContent\"]\n            ],\n\n            # Expensive chart that rarely changes\n            h.div(copy=not props.get(\"chartDataChanged\", False))[\n                h(ChartComponent, data=props[\"chartData\"])\n            ]\n        ]\n```\n\n## Learn More\n\n- **[Crank.js Documentation](https://crank.js.org/)** - The underlying framework\n- **[PyScript Guide](https://pyscript.net/)** - Running Python in browsers\n- **[Examples](examples/)** - See Crank.py in action\n\n## Contributing\n\nContributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md) first.\n\n## License\nMIT \u00a9 2025\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Python Frontend Framework with Async/Generators, Powered by Crank.js",
    "version": "0.2.0",
    "project_urls": {
        "Bug Tracker": "https://github.com/bikeshaving/crankpy/issues",
        "Documentation": "https://github.com/bikeshaving/crankpy#readme",
        "Homepage": "https://github.com/bikeshaving/crankpy",
        "JavaScript Core": "https://crank.js.org",
        "Repository": "https://github.com/bikeshaving/crankpy"
    },
    "split_keywords": [
        "crank",
        " components",
        " pyscript",
        " pyodide",
        " micropython",
        " generators",
        " ui",
        " hyperscript",
        " jsx",
        " browser"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "cf5c5c162d982376dbdc2a85bbeecbfe06a316d68780cb120a7636853427007b",
                "md5": "0c1be6f62ae4c76069dc4819a869e394",
                "sha256": "cd68b70859e5d772d898848e24b4392b99d6ac5ae2ecd2eb257fff7a94a72244"
            },
            "downloads": -1,
            "filename": "crankpy-0.2.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "0c1be6f62ae4c76069dc4819a869e394",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 16808,
            "upload_time": "2025-10-06T17:51:52",
            "upload_time_iso_8601": "2025-10-06T17:51:52.402403Z",
            "url": "https://files.pythonhosted.org/packages/cf/5c/5c162d982376dbdc2a85bbeecbfe06a316d68780cb120a7636853427007b/crankpy-0.2.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "737aa966fd3ae55ffdb7d5afb009be7fba6aa76840f973932b1f03ec67d478a5",
                "md5": "ecad4ab571e53e2d26ce702369dcb59e",
                "sha256": "6f8dd04a377a7525e859d131a155ad5a2f069b3f7f64ec9a564f164e5a6a6740"
            },
            "downloads": -1,
            "filename": "crankpy-0.2.0.tar.gz",
            "has_sig": false,
            "md5_digest": "ecad4ab571e53e2d26ce702369dcb59e",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 53037,
            "upload_time": "2025-10-06T17:51:53",
            "upload_time_iso_8601": "2025-10-06T17:51:53.461653Z",
            "url": "https://files.pythonhosted.org/packages/73/7a/a966fd3ae55ffdb7d5afb009be7fba6aa76840f973932b1f03ec67d478a5/crankpy-0.2.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-06 17:51:53",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "bikeshaving",
    "github_project": "crankpy",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "crankpy"
}
        
Elapsed time: 1.03275s