Skip to main content

Block-oriented HTML generation that works with ordinary control flow

Project description

🏷️ Tagflow

Tagflow is a Python library that generates HTML in a block-oriented way that works with ordinary control flow. Instead of nested function calls, it uses context managers and decorators to align HTML generation with Python's native language features.

So you can write HTML using familiar Python constructs: loops for repeated elements, conditionals for dynamic content, and try/except for error boundaries. Create reusable components with decorators and organize code into reusable components that yield to produce child content.

Most Python HTML generation libraries follow a pattern of nested call expressions to mirror HTML's tree structure, using lists, list comprehensions, and variable arguments to pass children. This forces us to abandon familiar control flow patterns (if, for, continue, try) in favor of functional-style composition, leading to convoluted code when dealing with conditional rendering, loops, or error handling.

Tagflow takes a different approach by using context managers to track the current position in the HTML tree, allowing developers to use ordinary Python control flow while building HTML documents step by step in nested with blocks.

We have not yet measured the performance of Tagflow, but it would be interesting to see how it compares to other Python HTML generation libraries. It's not optimized for speed but we haven't seen any major performance issues.

The current implementation uses Python's builtin ElementTree for maintaining the document tree and serializing it to HTML.

While streaming responses aren't implemented yet, Tagflow's design will support generating and sending HTML incrementally without buffering entire pages in memory.

Installation

Tagflow is available on PyPI with the name tagflow.

uv add tagflow

Usage

from tagflow import tag, text, document

def page(title: str):
  # open a <html> tag in the current document
  with tag.html(lang="en"):
      with tag.head():
          with tag.title():
              # append a text node to the title tag
              text(title)
          with tag.script(src="https://cdn.tailwindcss.com"):
              pass # no content in the script tag

      # use "classes" to avoid conflict with the "class" keyword
      with tag.body(classes="bg-gray-50 p-4"):
          with tag.h1():
              text("Welcome!")
          # positional arguments are also used as class names
          with tag.p("serif", "mb-4"):
              # another way to set attributes
              attr("contenteditable")
              attr("spellcheck", False)
              # classes can also be an iterable
              attr("class", ["text-lg text-gray-900", "font-bold"])
              # after emitting content you can't change the attributes
              text("This is a paragraph.")

def index_html() -> str:
  # document() is a context manager that creates a new document root
  with document() as root:
    page("Tagflow")
    return root.to_html()

You can use decorators to specify the tag structure for a function. This is a nice way to define reusable components.

from tagflow import tag, text, html

# decorator nesting lets us skip indentation for basic structure
@html.main(lang="en")
@html.article(classes="w-prose mx-auto")
def welcome(name: str):
    with tag.h1():
        text(f"Welcome, {name}!")
    with tag.p():
        text("This is a paragraph.")

To define a component that takes children, just define a context manager. Here we also show some control flow examples.

from tagflow import tag, text, html
from dataclasses import dataclass
from contextlib import contextmanager

@contextmanager
def page(title: str):
    with tag.html(lang="en"):
        with tag.head():
            with tag.title():
                text(title)
            with tag.script(src="https://cdn.tailwindcss.com"):
                pass

        with tag.body(classes="bg-gray-50 p-4"):
            yield

@contextmanager
def article():
    with tag.article(classes="w-prose mx-auto"):
        yield

@dataclass
class Post:
    id: str
    title: str
    content: list[str]

def index(posts: list[Post]):
    with page("Posts"):
        # just use a loop; no special iteration feature
        for post in posts:
            # just use if; no special blank feature
            if not post.content:
                continue

            # just use try; no special error boundary feature
            try:
                render_post(post)
            except ValueError as e:
                with tag.p(classes="text-red-500"):
                    text("Error rendering post: ")
                    text(str(e))

@html.article()
def render_post(post: Post):
    if not post.id:
        raise ValueError("Post ID is required")

    attr("id", post.id)
    with tag.h1():
        text(post.title)

    for block in post.content:
        with tag.p(classes="mb-4"):
            text(block)

FastAPI response class

Tagflow provides a custom FastAPI response class and middleware that make it easy to integrate with FastAPI endpoints. The middleware automatically sets up a fresh document context for each request, while the response class handles rendering:

from fastapi import FastAPI
from tagflow import tag, text, DocumentMiddleware, TagResponse

app = FastAPI()
app.add_middleware(DocumentMiddleware)

@app.get("/", response_class=TagResponse)
def home():
    with tag.html(lang="en"):
        with tag.head():
            with tag.title():
                text("Home")
        with tag.body():
            with tag.h1():
                text("Welcome!")

# Works with async endpoints too
@app.get("/posts/{id}", response_class=TagResponse)
async def view_post(id: str):
    post = await get_post(id)
    with tag.html():
        with tag.body():
            render_post(post)

You can also set the FastAPI default response class to TagResponse in your FastAPI app.

from fastapi import FastAPI
from tagflow import TagResponse

app = FastAPI(default_response_class=TagResponse)

@app.get("/")
def home():
    with tag.html():
        with tag.body():
            with tag.h1():
                text("Welcome!")

License

Tagflow is open source software released under the MIT license. See the LICENSE file for more details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tagflow-0.5.0.tar.gz (25.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tagflow-0.5.0-py3-none-any.whl (6.0 kB view details)

Uploaded Python 3

File details

Details for the file tagflow-0.5.0.tar.gz.

File metadata

  • Download URL: tagflow-0.5.0.tar.gz
  • Upload date:
  • Size: 25.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.1

File hashes

Hashes for tagflow-0.5.0.tar.gz
Algorithm Hash digest
SHA256 c64c63afbdac36b36697c66a09027060e1e7e760a7d37dd4512d3bb5e2dcbd6e
MD5 620a2694a15d155faec48812d76f16ad
BLAKE2b-256 5f202516361adb42e5597c1982fff6f7bfde214ff72ca9598aa244a5a931c7ab

See more details on using hashes here.

File details

Details for the file tagflow-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: tagflow-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 6.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.1

File hashes

Hashes for tagflow-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6ac1e9824a398df9c41ab8ea9962cdfe99cfe7b95dfca6b06a1f4b09c022b2a2
MD5 6a8c766906e7aa484ebd3b5f25866a8b
BLAKE2b-256 ec4508918996cd51cc89540a599cc9cc947db4b0d30e8f1a259f33a90593e687

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page