Código fuente : https://github.com/volfpeter/htmy
Documentación y ejemplos : https://volfpeter.github.io/htmy
htmyAsync , Pure-Python Rendering Engine.
ErrorBoundary incorporado, fácil de usar para el manejo de errores elegantes.El paquete está disponible en PYPI y se puede instalar con:
$ pip install htmy Toda la biblioteca, desde el motor de representación hasta los componentes incorporados, se construye alrededor de algunos protocolos simples y un puñado de clases de utilidad simples. Esto significa que puede personalizar, extender o reemplazar fácilmente básicamente todo en la biblioteca. Sí, incluso el motor de renderizado. Las partes restantes seguirán funcionando como se esperaba.
Además, la biblioteca no se basa en características avanzadas de Python como metaclase o descriptores. Tampoco hay clases base complejas y similares. Incluso un ingeniero junior podría entender, desarrollar y depurar una aplicación construida con htmy .
Cada clase con una sincronización o asíncrata htmy(context: Context) -> Component es un componente htmy (técnicamente un HTMYComponentType ). Las cadenas también son componentes, así como listas o tuplas de objetos HTMYComponentType o String.
El uso de este nombre de método permite la conversión de cualquiera de sus objetos comerciales (desde TypedDicts s o modelos pydantic en clases ORM) en componentes sin temor a la colisión de nombres con otras herramientas.
El soporte de Async permite cargar datos o ejecutar Async Business Logic directamente en sus componentes. Esto puede reducir la cantidad de plantilla que necesita escribir en algunos casos, y también le brinda la libertad de dividir la lógica de representación y no representación de cualquier manera que mejor le parezca.
Ejemplo:
from dataclasses import dataclass
from htmy import Component , Context , html
@ dataclass ( frozen = True , kw_only = True , slots = True )
class User :
username : str
name : str
email : str
async def is_admin ( self ) -> bool :
return False
class UserRow ( User ):
async def htmy ( self , context : Context ) -> Component :
role = "admin" if await self . is_admin () else "restricted"
return html . tr (
html . td ( self . username ),
html . td ( self . name ),
html . td ( html . a ( self . email , href = f"mailto: { self . email } " )),
html . td ( role )
)
@ dataclass ( frozen = True , kw_only = True , slots = True )
class UserRows :
users : list [ User ]
def htmy ( self , context : Context ) -> Component :
# Note that a list is returned here. A list or tuple of `HTMYComponentType | str` objects is also a component.
return [ UserRow ( username = u . username , name = u . name , email = u . email ) for u in self . users ]
user_table = html . table (
UserRows (
users = [
User ( username = "Foo" , name = "Foo" , email = "[email protected]" ),
User ( username = "Bar" , name = "Bar" , email = "[email protected]" ),
]
)
) htmy también proporciona un decorador @component que puede usarse en Sync o Async my_component(props: MyProps, context: Context) -> Component para convertirlos en componentes (preservando la escritura de props ).
Aquí está el mismo ejemplo que el anterior, pero con los componentes de la función:
from dataclasses import dataclass
from htmy import Component , Context , component , html
@ dataclass ( frozen = True , kw_only = True , slots = True )
class User :
username : str
name : str
email : str
async def is_admin ( self ) -> bool :
return False
@ component
async def user_row ( user : User , context : Context ) -> Component :
# The first argument of function components is their "props", the data they need.
# The second argument is the rendering context.
role = "admin" if await user . is_admin () else "restricted"
return html . tr (
html . td ( user . username ),
html . td ( user . name ),
html . td ( html . a ( user . email , href = f"mailto: { user . email } " )),
html . td ( role )
)
@ component
def user_rows ( users : list [ User ], context : Context ) -> Component :
# Nothing to await in this component, so it's sync.
# Note that we only pass the "props" to the user_row() component (well, function component wrapper).
# The context will be passed to the wrapper during rendering.
return [ user_row ( user ) for user in users ]
user_table = html . table (
user_rows (
[
User ( username = "Foo" , name = "Foo" , email = "[email protected]" ),
User ( username = "Bar" , name = "Bar" , email = "[email protected]" ),
]
)
) htmy tiene un rico conjunto de utilidades y componentes incorporados tanto para HTML como para otros casos de uso:
html : un conjunto completo de etiquetas HTML de línea de base.md : Utilidad MarkdownParser y componente MD para cargar, analizar, convertir y representar el contenido de Markdown.i18n : Utilidades para Async, internacionalización basada en JSON.BaseTag , TagWithProps , Tag , WildcardTag : clases base para etiquetas XML personalizadas.ErrorBoundary , Fragment , SafeStr , WithContext : Utilidades para el manejo de errores, envoltorios de componentes, proveedores de contexto y formateo.Snippet : clase de utilidad para cargar y personalizar fragmentos de documentos desde el sistema de archivos.etree.ETreeConverter : utilidad que convierte XML en un árbol de componentes con soporte para componentes HTMY personalizados. htmy.HTMY es el renderizador predeterminado incorporado de la biblioteca.
Si está utilizando la biblioteca en un marco web de Async como Fastapi, entonces ya está en un entorno async, por lo que puede representar componentes tan simplemente como esto: await HTMY().render(my_root_component) .
Si está tratando de ejecutar el renderizador en un entorno de sincronización, como un script local o un CLI, entonces primero debe envolver el renderizador en una tarea de async y ejecutar esa tarea con asyncio.run() ::
import asyncio
from htmy import HTMY , html
async def render_page () -> None :
page = (
html . DOCTYPE . html ,
html . html (
html . body (
html . h1 ( "Hello World!" ),
html . p ( "This page was rendered by " , html . code ( "htmy" )),
),
)
)
result = await HTMY (). render ( page )
print ( result )
if __name__ == "__main__" :
asyncio . run ( render_page ()) Como puede ver en los ejemplos de código anteriores, cada componente tiene un argumento context: Context , que hasta ahora no hemos usado. El contexto es una forma de compartir datos con todo el subárbol de un componente sin "perforación".
El contexto (técnicamente un Mapping ) es administrado por completo por el renderizador. Componentes del proveedor de contexto (cualquier clase con una sincronización o async htmy_context() -> Context ) Agregue nuevos datos al contexto para ponerlo a disposición de los componentes en su subárbol, y los componentes simplemente pueden tomar lo que necesitan del contexto.
No hay restricción en lo que puede estar en el contexto, se puede usar para cualquier cosa que la aplicación necesita, por ejemplo, hacer que el usuario actual, las preferencias de interfaz de usuario, los temas o los formatúas estén disponibles para los componentes. De hecho, los componentes incorporados obtienen su Formatter del contexto si contiene uno, para que sea posible personalizar el nombre de la propiedad de la etiqueta y el formato de valor.
Aquí hay un ejemplo de implementación de proveedores de contexto y del consumidor:
import asyncio
from htmy import HTMY , Component , ComponentType , Context , component , html
class UserContext :
def __init__ ( self , * children : ComponentType , username : str , theme : str ) -> None :
self . _children = children
self . username = username
self . theme = theme
def htmy_context ( self ) -> Context :
# Context provider implementation.
return { UserContext : self }
def htmy ( self , context : Context ) -> Component :
# Context providers must also be components, as they just
# wrap some children components in their context.
return self . _children
@ classmethod
def from_context ( cls , context : Context ) -> "UserContext" :
user_context = context [ cls ]
if isinstance ( user_context , UserContext ):
return user_context
raise TypeError ( "Invalid user context." )
@ component
def welcome_page ( text : str , context : Context ) -> Component :
# Get user information from the context.
user = UserContext . from_context ( context )
return (
html . DOCTYPE . html ,
html . html (
html . body (
html . h1 ( text , html . strong ( user . username )),
data_theme = user . theme ,
),
),
)
async def render_welcome_page () -> None :
page = UserContext (
welcome_page ( "Welcome back " ),
username = "John" ,
theme = "dark" ,
)
result = await HTMY (). render ( page )
print ( result )
if __name__ == "__main__" :
asyncio . run ( render_welcome_page ()) Por supuesto, puede confiar en las utilidades relacionadas con el contexto incorporadas, como las clases ContextAware o WithContext para el uso de contexto conveniente y tipado con menos código de planta.
Como se mencionó anteriormente, la clase Formatter incorporada es responsable del nombre del atributo de etiqueta y el formato de valor. Puede anular por completo o extender el comportamiento de formato incorporado simplemente extendiendo esta clase o agregando nuevas reglas a una instancia de ella, y luego agregando la instancia personalizada al contexto, ya sea directamente en HTMY o HTMY.render() , o en un componente de proveedor de contexto.
Estas son reglas de formato de atributo de etiqueta predeterminadas:
_ -> - ) a menos que el nombre del atributo comience o termine con un subrayador, en cuyo caso se eliminan los subrayos liderantes y finales y se conserva el resto del nombre del atributo. Por ejemplo, data_theme="dark" se convierte en data-theme="dark" , pero _data_theme="dark" terminará como data_theme="dark" en el texto renderizado. Más importante aún, class_="text-danger" , _class="text-danger" , _class__="text-danger" class="text-danger" , y _for="my-input" o for_="my_input" se convertirá for="my-input" .bool se convierten en cadenas ( "true" y "false" ).XBool.true se convierten en una cadena vacía, y los valores de XBool.false se omiten (solo se representa el nombre del atributo).date y el atributo datetime se convierten en cadenas ISO. El componente ErrorBoundary es útil si desea que su aplicación falle con gracia (por ejemplo, muestra un mensaje de error) en lugar de aumentar un error HTTP.
El límite de error envuelve un subárbol componente componente. Cuando el renderizador encuentre un componente ErrorBoundary , intentará representar su contenido envuelto. Si la renderización falla con una excepción en cualquier punto del subárbol de error de ErrorBoundary , el renderizador volverá automáticamente al componente que asignó a la propiedad de fallback de ErrorBoundary .
Opcionalmente, puede definir qué errores puede manejar un límite de error, lo que le brinda un buen control sobre el manejo de errores.
En general, un componente debe ser asíncrono si debe esperar una llamada asíncrata adentro.
Si un componente ejecuta una llamada sincrónica potencialmente "de larga duración", se recomienda encarecidamente delegar esa llamada a un hilo de trabajador y lo espera (lo que hace que el componente asíncrito). Esto se puede hacer, por ejemplo, con la utilidad to_thread de anyio , starlette (o fastapi 's) run_in_threadpool() , y así sucesivamente. El objetivo aquí es evitar bloquear el bucle de eventos de Asyncio, ya que eso puede conducir a problemas de rendimiento.
En todos los demás casos, es mejor usar componentes de sincronización.
Fastapi:
En un extremo del espectro, existen los marcos completos de aplicaciones que combinan las aplicaciones del servidor (Python) y el cliente (JavaScript) con toda la administración del estado y la sincronización en un solo paquete Python (en algunos casos un JavaScript adicional). Algunos de los ejemplos más populares son: Reflex, NiceGui, Reactpy y Fastui.
El principal beneficio de estos marcos es la prototipos de aplicaciones rápidas y una experiencia de desarrollador muy conveniente (al menos siempre que permanezca dentro del conjunto de características incorporadas del marco). A cambio de eso, son muy obstinados (desde los componentes hasta las herramientas frontendas y la gestión del estado), la ingeniería subyacente es muy compleja, la implementación y la escala pueden ser difíciles o costosas, y pueden ser difíciles de migrar. Incluso con estas advertencias, pueden ser una muy buena opción para herramientas internas y creación de prototipos de aplicaciones.
El otro extremo del espectro, motores de renderizado simple, está dominado por el motor de plantilla Jinja, que es una opción segura como lo ha sido y estará presente durante mucho tiempo. Los principales inconvenientes con Jinja son la falta de un buen soporte IDE, la falta completa de soporte de análisis de código estático y la (subjetivamente) fea sintaxis.
Luego, existen herramientas que apuntan a la Middleground, generalmente al proporcionar la mayoría de los beneficios y inconvenientes de los marcos completos de aplicaciones al tiempo que deja la administración estatal, la comunicación del cliente cliente y las actualizaciones dinámicas de la interfaz de usuario para que el usuario la resuelva, a menudo con cierto nivel de soporte HTMX. Este grupo incluye bibliotecas como Fasthtml y Ludic.
El objetivo principal de htmy es ser un motor async y de renderizado de Python Pure, que es lo más simple , mantenible y personalizable como sea posible, al tiempo que proporciona todos los bloques de construcción para crear (convenientemente) aplicaciones complejas y mantenibles.
La biblioteca tiene como objetivo minimizar sus dependencias. Actualmente se requieren las siguientes dependencias:
anyio : para operaciones de archivos async y redes.async-lru : para el almacenamiento en caché de Async.markdown : para el análisis de Markdown. Use ruff para pelucas y formateo, mypy para el análisis de código estático y pytest para las pruebas.
La documentación está construida con mkdocs-material y mkdocstrings .
Todas las contribuciones son bienvenidas, que incluyen más documentación, ejemplos, código y pruebas. Incluso preguntas.
El paquete es de código abierto en las condiciones de la licencia MIT.