Skip to content

Commit

Permalink
Merge pull request #25 from volfpeter/renderer-performance-improvement
Browse files Browse the repository at this point in the history
New default renderer with 2-6x better performance
  • Loading branch information
volfpeter authored Dec 28, 2024
2 parents 0322a39 + 3c3adef commit e6605f5
Show file tree
Hide file tree
Showing 25 changed files with 554 additions and 80 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,16 @@ user_table = html.table(

### Rendering

`htmy.HTMY` is the built-in, default renderer of the library.
`htmy.Renderer` is the built-in, default renderer of the library.

If you're using the library in an async web framework like [FastAPI](https://fastapi.tiangolo.com/), then you're already in an async environment, so you can render components as simply as this: `await HTMY().render(my_root_component)`.
If you're using the library in an async web framework like [FastAPI](https://fastapi.tiangolo.com/), then you're already in an async environment, so you can render components as simply as this: `await Renderer().render(my_root_component)`.

If you're trying to run the renderer in a sync environment, like a local script or CLI, then you first need to wrap the renderer in an async task and execute that task with `asyncio.run()`:

```python
import asyncio

from htmy import HTMY, html
from htmy import Renderer, html

async def render_page() -> None:
page = (
Expand All @@ -173,7 +173,7 @@ async def render_page() -> None:
)
)

result = await HTMY().render(page)
result = await Renderer().render(page)
print(result)


Expand All @@ -194,7 +194,7 @@ Here's an example context provider and consumer implementation:
```python
import asyncio

from htmy import HTMY, Component, ComponentType, Context, component, html
from htmy import Component, ComponentType, Context, Renderer, component, html

class UserContext:
def __init__(self, *children: ComponentType, username: str, theme: str) -> None:
Expand Down Expand Up @@ -240,7 +240,7 @@ async def render_welcome_page() -> None:
theme="dark",
)

result = await HTMY().render(page)
result = await Renderer().render(page)
print(result)

if __name__ == "__main__":
Expand All @@ -251,7 +251,7 @@ You can of course rely on the built-in context related utilities like the `Conte

### Formatter

As mentioned before, the built-in `Formatter` class is responsible for tag attribute name and value formatting. You can completely override or extend the built-in formatting behavior simply by extending this class or adding new rules to an instance of it, and then adding the custom instance to the context, either directly in `HTMY` or `HTMY.render()`, or in a context provider component.
As mentioned before, the built-in `Formatter` class is responsible for tag attribute name and value formatting. You can completely override or extend the built-in formatting behavior simply by extending this class or adding new rules to an instance of it, and then adding the custom instance to the context, either directly in `Renderer` or `Renderer.render()`, or in a context provider component.

These are default tag attribute formatting rules:

Expand Down
4 changes: 4 additions & 0 deletions docs/api/renderer/baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# ::: htmy.renderer.baseline

options:
show_root_heading: true
2 changes: 1 addition & 1 deletion docs/api/renderer.md → docs/api/renderer/default.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ::: htmy.renderer
# ::: htmy.renderer.default

options:
show_root_heading: true
14 changes: 7 additions & 7 deletions docs/examples/fastapi-htmx-tailwind-daisyui.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ from typing import Annotated
from fastapi import Depends, FastAPI, Request
from fastapi.responses import HTMLResponse

from htmy import HTMY, Component, ComponentType, Context, component, html, is_component_sequence
from htmy import Component, ComponentType, Context, Renderer, component, html, is_component_sequence


@dataclass
Expand All @@ -30,7 +30,7 @@ class User:


def make_htmy_context(request: Request) -> Context:
"""Creates the base HTMY context for rendering."""
"""Creates the base htmy context for rendering."""
# The context will map the `Request` type to the current request and the User class
# to the current user. This is similar to what the `ContextAware` utility does, but
# simpler. With this context, components will be able to easily access the request
Expand All @@ -42,14 +42,14 @@ RendererFunction = Callable[[Component], Awaitable[HTMLResponse]]


def render(request: Request) -> RendererFunction:
"""FastAPI dependency that returns an HTMY renderer function."""
"""FastAPI dependency that returns an htmy renderer function."""

async def exec(component: Component) -> HTMLResponse:
# Note that we add the result of `make_htmy_context()` as the default context to the
# `HTMY` renderer. This way wherever this function is used for rendering in routes,
# Note that we add the result of `make_htmy_context()` as the default context to
# the renderer. This way wherever this function is used for rendering in routes,
# every rendered component will be able to access the current request and user.
htmy = HTMY(make_htmy_context(request))
return HTMLResponse(await htmy.render(component))
renderer = Renderer(make_htmy_context(request))
return HTMLResponse(await renderer.render(component))

return exec

Expand Down
10 changes: 5 additions & 5 deletions docs/examples/internationalization.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ The focus of this example is using the built-in `I18n` utility for international

First of all, we must create some translation resources (plain JSON files). Let's do this by creating the `locale/en/page` folder structure and adding a `hello.json` in the `page` folder with the following content: `{ "message": "Hey {name}" }`. Notice the Python format string in the value for the `"message"` key, such strings can be automatically formatted by `I18n`, see the details in the docs and in the usage example below.

Using `I18n` consists of only two steps: create an `I18n` instance, and include it in the `HTMY` rendering context so it can be accessed by components in their `htmy()` (render) method.
Using `I18n` consists of only two steps: create an `I18n` instance, and include it in the rendering context so it can be accessed by components in their `htmy()` (render) method.

With the translation resource in place, we can create the `app.py` file and implement our translated components like this:

```python
import asyncio
from pathlib import Path

from htmy import HTMY, Component, Context, html
from htmy import Component, Context, Renderer, html
from htmy.i18n import I18n


class TranslatedComponent:
"""HTMY component with translated content."""
"""Component with translated content."""

async def htmy(self, context: Context) -> Component:
# Get the I18n instance from the rendering context.
Expand All @@ -39,14 +39,14 @@ base_folder = Path(__file__).parent

i18n = I18n(base_folder / "locale" / "en")
"""
The `I18n` instance that we can add to the `HTMY` rendering context.
The `I18n` instance that we can add to the rendering context.
It takes translations from the `locale/en` folder.
"""


async def render_hello() -> None:
rendered = await HTMY().render(
rendered = await Renderer().render(
# Render a TranslatedComponent.
TranslatedComponent(),
# Include the created I18n instance in the rendering context.
Expand Down
10 changes: 5 additions & 5 deletions docs/examples/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ Then we can create the most minimal version of `app.py` that will be responsible
```python
import asyncio

from htmy import HTMY, md
from htmy import Renderer, md


async def render_post() -> None:
md_post = md.MD("post.md") # Create an htmy.md.MD component.
rendered = await HTMY().render(md_post) # Render the MD component.
rendered = await Renderer().render(md_post) # Render the MD component.
print(rendered) # Print the result.


Expand All @@ -65,7 +65,7 @@ The `post.md` file can remain the same as above, but `app.py` will change quite
First of all we need a few more import (although some only for typing):

```python
from htmy import HTMY, Component, ComponentType, Context, PropertyValue, etree, html, md
from htmy import Component, ComponentType, Context, PropertyValue, Renderer, etree, html, md
```

Next we need a `Page` component that defines the base HTML structure of the webpage:
Expand Down Expand Up @@ -156,7 +156,7 @@ async def render_post() -> None:
converter=md_converter.convert, # And make it use our element converter's conversion method.
)
page = Page(md_post) # Wrap the post in a Page component.
rendered = await HTMY().render(page) # Render the MD component.
rendered = await Renderer().render(page) # Render the MD component.
print(rendered) # Print the result.
```

Expand Down Expand Up @@ -202,7 +202,7 @@ Then we can create the `PostInfo` `htmy` component:

```python
class PostInfo:
"""HTMY component for post info rendering."""
"""Component for post info rendering."""

def __init__(self, author: str, published_at: str) -> None:
self.author = author
Expand Down
14 changes: 7 additions & 7 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,16 @@ user_table = html.table(

### Rendering

`htmy.HTMY` is the built-in, default renderer of the library.
`htmy.Renderer` is the built-in, default renderer of the library.

If you're using the library in an async web framework like [FastAPI](https://fastapi.tiangolo.com/), then you're already in an async environment, so you can render components as simply as this: `await HTMY().render(my_root_component)`.
If you're using the library in an async web framework like [FastAPI](https://fastapi.tiangolo.com/), then you're already in an async environment, so you can render components as simply as this: `await Renderer().render(my_root_component)`.

If you're trying to run the renderer in a sync environment, like a local script or CLI, then you first need to wrap the renderer in an async task and execute that task with `asyncio.run()`:

```python
import asyncio

from htmy import HTMY, html
from htmy import Renderer, html

async def render_page() -> None:
page = (
Expand All @@ -173,7 +173,7 @@ async def render_page() -> None:
)
)

result = await HTMY().render(page)
result = await Renderer().render(page)
print(result)


Expand All @@ -194,7 +194,7 @@ Here's an example context provider and consumer implementation:
```python
import asyncio

from htmy import HTMY, Component, ComponentType, Context, component, html
from htmy import Component, ComponentType, Context, Renderer, component, html

class UserContext:
def __init__(self, *children: ComponentType, username: str, theme: str) -> None:
Expand Down Expand Up @@ -240,7 +240,7 @@ async def render_welcome_page() -> None:
theme="dark",
)

result = await HTMY().render(page)
result = await Renderer().render(page)
print(result)

if __name__ == "__main__":
Expand All @@ -251,7 +251,7 @@ You can of course rely on the built-in context related utilities like the `Conte

### Formatter

As mentioned before, the built-in `Formatter` class is responsible for tag attribute name and value formatting. You can completely override or extend the built-in formatting behavior simply by extending this class or adding new rules to an instance of it, and then adding the custom instance to the context, either directly in `HTMY` or `HTMY.render()`, or in a context provider component.
As mentioned before, the built-in `Formatter` class is responsible for tag attribute name and value formatting. You can completely override or extend the built-in formatting behavior simply by extending this class or adding new rules to an instance of it, and then adding the custom instance to the context, either directly in `Renderer` or `Renderer.render()`, or in a context provider component.

These are default tag attribute formatting rules:

Expand Down
8 changes: 4 additions & 4 deletions examples/internationalization/app.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import asyncio
from pathlib import Path

from htmy import HTMY, Component, Context, html
from htmy import Component, Context, Renderer, html
from htmy.i18n import I18n


class TranslatedComponent:
"""HTMY component with translated content."""
"""Component with translated content."""

async def htmy(self, context: Context) -> Component:
# Get the I18n instance from the rendering context.
Expand All @@ -25,14 +25,14 @@ async def htmy(self, context: Context) -> Component:

i18n = I18n(base_folder / "locale" / "en")
"""
The `I18n` instance that we can add to the `HTMY` rendering context.
The `I18n` instance that we can add to the rendering context.
It takes translations from the `locale/en` folder.
"""


async def render_hello() -> None:
rendered = await HTMY().render(
rendered = await Renderer().render(
# Render a TranslatedComponent.
TranslatedComponent(),
# Include the created I18n instance in the rendering context.
Expand Down
6 changes: 3 additions & 3 deletions examples/markdown_customization/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio

from htmy import HTMY, Component, ComponentType, Context, PropertyValue, etree, html, md
from htmy import Component, ComponentType, Context, PropertyValue, Renderer, etree, html, md


class Page:
Expand Down Expand Up @@ -34,7 +34,7 @@ def htmy(self, context: Context) -> Component:


class PostInfo:
"""HTMY component for post info rendering."""
"""Component for post info rendering."""

def __init__(self, author: str, published_at: str) -> None:
self.author = author
Expand Down Expand Up @@ -91,7 +91,7 @@ async def render_post() -> None:
converter=md_converter.convert, # And make it use our element converter's conversion method.
)
page = Page(md_post) # Wrap the post in a Page component.
rendered = await HTMY().render(page) # Render the MD component.
rendered = await Renderer().render(page) # Render the MD component.
print(rendered) # Print the result.


Expand Down
4 changes: 2 additions & 2 deletions examples/markdown_essentials/app.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import asyncio

from htmy import HTMY, md
from htmy import Renderer, md


async def render_post() -> None:
md_post = md.MD("post.md") # Create an htmy.md.MD component.
rendered = await HTMY().render(md_post) # Render the MD component.
rendered = await Renderer().render(md_post) # Render the MD component.
print(rendered) # Print the result.


Expand Down
5 changes: 4 additions & 1 deletion htmy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from .core import XBool as XBool
from .core import component as component
from .core import xml_format_string as xml_format_string
from .renderer import HTMY as HTMY
from .renderer import Renderer as Renderer
from .typing import AsyncComponent as AsyncComponent
from .typing import AsyncContextProvider as AsyncContextProvider
from .typing import AsyncFunctionComponent as AsyncFunctionComponent
Expand All @@ -36,3 +36,6 @@
from .typing import SyncFunctionComponent as SyncFunctionComponent
from .typing import is_component_sequence as is_component_sequence
from .utils import join_components as join_components

HTMY = Renderer
"""Deprecated alias for `Renderer`."""
6 changes: 3 additions & 3 deletions htmy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def __init__(self, *children: ComponentType, context: Context) -> None:
self._context = context

def htmy_context(self) -> Context:
"""Returns an HTMY context for child rendering."""
"""Returns the context for child rendering."""
return self._context


Expand Down Expand Up @@ -174,7 +174,7 @@ async def _get_text_content(self) -> str:
def _render_text(self, text: str, context: Context) -> Component:
"""
Render function that takes the text that must be rendered and the current rendering context,
and returns the corresponding HTMY component.
and returns the corresponding component.
"""
return SafeStr(text)

Expand Down Expand Up @@ -523,7 +523,7 @@ def htmy_name(self) -> str:

@abc.abstractmethod
def htmy(self, context: Context) -> Component:
"""Abstract base method for HTMY rendering."""
"""Abstract base component implementation."""
...

def _get_htmy_name(self) -> str:
Expand Down
Loading

0 comments on commit e6605f5

Please sign in to comment.