Código fonte : https://github.com/volfpeter/htmy
Documentação e exemplos : https://volfpeter.github.io/htmy
htmyMotor de renderização assíncrono , puro-píton .
ErrorBoundary e fácil de usar para o manuseio de erros graciosos.O pacote está disponível no Pypi e pode ser instalado com:
$ pip install htmy Toda a biblioteca-desde o próprio mecanismo de renderização até os componentes embutidos-é construído em torno de alguns protocolos simples e um punhado de classes de utilidade simples. Isso significa que você pode personalizar, estender ou substituir basicamente tudo na biblioteca. Sim, até o mecanismo de renderização. As peças restantes continuarão funcionando conforme o esperado.
Além disso, a biblioteca não depende de recursos avançados de Python, como metaclasses ou descritores. Também não há classes base complexas e coisas do gênero. Até um engenheiro júnior poderia entender, desenvolver e depurar um aplicativo construído com htmy .
Cada classe com um método de htmy(context: Context) -> Component é um componente htmy (tecnicamente um HTMYComponentType ). Strings também são componentes, além de listas ou tuplas de objetos HTMYComponentType ou String.
O uso deste nome do método permite a conversão de qualquer um dos seus objetos de negócios (dos modelos TypedDicts s ou pydantic em classes ORM) em componentes sem o medo de colisão de nome com outras ferramentas.
O suporte assíncrono permite carregar dados ou executar a lógica de negócios assíncronos corretamente em seus componentes. Isso pode reduzir a quantidade de caldeira que você precisa escrever em alguns casos e também oferece a liberdade de dividir a lógica de renderização e não renderização da maneira que você achar melhor.
Exemplo:
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 também fornece um decorador @component que pode ser usado no Sync ou ASYNC my_component(props: MyProps, context: Context) -> Component para convertê -los em componentes (preservando a digitação props ).
Aqui está o mesmo exemplo acima, mas com componentes de função:
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 tem um rico conjunto de utilitários e componentes internos para HTML e outros casos de uso:
html : um conjunto completo de tags HTML de linha de base.md : Utilitário MarkdownParser e componente MD para carregar, analisar, converter e renderizar conteúdo de marcação.i18n : Utilitários para a internacionalização baseada em JSON, JSON.BaseTag , TagWithProps , Tag , WildcardTag : Classes base para tags XML personalizadas.ErrorBoundary , Fragment , SafeStr , WithContext : Utilitários para manuseio de erros, invólucros de componentes, provedores de contexto e formatação.Snippet : classe utilitária para carregar e personalizar trechos de documentos do sistema de arquivos.etree.ETreeConverter : Utilitário que converte XML em uma árvore de componentes com suporte para componentes HTMY personalizados. htmy.HTMY é o renderizador padrão interno da biblioteca.
Se você estiver usando a biblioteca em uma estrutura da Web assíncrona como o FASTAPI, você já está em um ambiente assíncrono, para que você possa renderizar componentes exatamente como o seguinte: await HTMY().render(my_root_component) .
Se você está tentando executar o renderizador em um ambiente de sincronização, como um script local ou CLI, primeiro precisará envolver o renderizador em uma tarefa assíncrona e executar essa tarefa com 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 você pode ver nos exemplos de código acima, todo componente tem um context: Context , que não usamos até agora. O contexto é uma maneira de compartilhar dados com toda a subárvore de um componente sem a "perfuração de prop".
O contexto (tecnicamente um Mapping ) é inteiramente gerenciado pelo renderizador. Componentes do provedor de contexto (qualquer classe com uma sincronização ou assíncrono htmy_context() -> Context Método) Adicione novos dados ao contexto para disponibilizá -los aos componentes em sua subárvore, e os componentes podem simplesmente pegar o que precisam do contexto.
Não há restrição ao que pode ser no contexto, ele pode ser usado para qualquer coisa que o aplicativo precisa, por exemplo, tornando o usuário atual, as preferências da interface do usuário, os temas ou os formatados disponíveis para componentes. De fato, os componentes embutidos recebem seu Formatter do contexto, se ele contiver um, para possibilitar a personalização do nome da propriedade Tag e da formatação do valor.
Aqui está um exemplo de provedor de contexto e implementação do 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 ()) É claro que você pode confiar nos utilitários relacionados ao contexto interno, como as classes ContextAware ou WithContext , para uso de contexto conveniente e digitado com o código menos caldeira.
Como mencionado anteriormente, a classe Formatter interna é responsável pelo nome do atributo de tag e formatação de valor. Você pode substituir completamente ou estender o comportamento de formatação interno simplesmente estendendo essa classe ou adicionando novas regras a uma instância e, em seguida, adicionando a instância personalizada ao contexto, diretamente no HTMY ou HTMY.render() ou em um componente de provedor de contexto.
Essas são regras de formatação de atributo de tag padrão:
_ -> - ), a menos que o nome do atributo seja iniciado ou termine com um sublinhado; nesse caso, liderar e trocar os sublinhados são removidos e o restante do nome do atributo é preservado. Por exemplo, data_theme="dark" é convertido em data-theme="dark" , mas _data_theme="dark" acabará como data_theme="dark" no texto renderizado. Mais importante, class_="text-danger" , _class="text-danger" , _class__="text-danger" são todos convertidos em class="text-danger" e _for="my-input" ou for_="my_input" se tornará for="my-input" .bool são convertidos em strings ( "true" e "false" ).XBool.true são convertidos em uma string vazia e os valores XBool.false são ignorados (apenas o nome do atributo é renderizado).date e datetime são convertidos em strings ISO. O componente ErrorBoundary é útil se você deseja que seu aplicativo falhe graciosamente (por exemplo, exiba uma mensagem de erro) em vez de aumentar um erro HTTP.
O limite de erro envolve um componente Subltse. Quando o renderizador encontrar um componente ErrorBoundary , ele tentará renderizar seu conteúdo embrulhado. Se a renderização falhar com uma exceção em qualquer ponto da subárvore do ErrorBoundary , o renderizador voltará automaticamente ao componente que você atribuiu à propriedade fallback do ErrorBoundary .
Opcionalmente, você pode definir quais erros um limite de erro pode lidar, fornecendo um bom controle sobre o manuseio de erros.
Em geral, um componente deve ser assíncrono se deve aguardar alguma chamada assíncrona dentro.
Se um componente executar uma chamada síncrona potencialmente "de longa duração", é fortemente recomendado delegar essa chamada para um fio de trabalhador e aguarda (tornando o componente assíncrono). Isso pode ser feito, por exemplo, com o utilitário to_thread de anyio , RUN_THREADPOOL (ou fastapi ) de starlette run_in_threadpool() e assim por diante. O objetivo aqui é evitar bloquear o loop de eventos Asyncio, pois isso pode levar a problemas de desempenho.
Em todos os outros casos, é melhor usar componentes de sincronização.
FASTAPI:
Em uma extremidade do espectro, existem as estruturas de aplicativos completas que combinam os aplicativos servidores (Python) e cliente (JavaScript) com todo o gerenciamento de estado e sincronização em um único pacote Python (e em alguns casos um JavaScript adicional). Alguns dos exemplos mais populares são: reflexo, nicegui, reactpy e fastui.
O principal benefício dessas estruturas é a prototipagem rápida de aplicativos e uma experiência de desenvolvedor muito conveniente (pelo menos enquanto você permanecer dentro do conjunto de recursos interno da estrutura). Em troca disso, eles são muito opinativos (dos componentes a ferramentas de front -end e gerenciamento de estado), a engenharia subjacente é muito complexa, a implantação e a escala podem ser difíceis ou caras e podem ser difíceis de migrar para longe. Mesmo com essas advertências, elas podem ser uma opção muito boa para ferramentas internas e prototipagem de aplicativos.
A outra extremidade do espectro - motores de renderização simples - é dominada pelo motor de modelos Jinja, o que é uma escolha segura como tem sido e estará por muito tempo. As principais desvantagens do Jinja são a falta de bom suporte de IDE, a completa falta de suporte da análise de código estático e a sintaxe (subjetivamente) feia.
Depois, existem ferramentas que buscam o meio do meio, geralmente fornecendo a maioria dos benefícios e desvantagens de estruturas completas de aplicativos, deixando o gerenciamento do estado, a comunicação do cliente-servidor e as atualizações dinâmicas da interface do usuário para o usuário resolver, geralmente com algum nível de suporte HTMX. Este grupo inclui bibliotecas como Fasthtml e Lúdica.
O objetivo principal do htmy é ser um mecanismo de renderização assíncrono e puro-python, que é o mais simples , sustentável e personalizável possível, enquanto ainda fornece todos os blocos de construção para (convenientemente) a criação de aplicações complexas e sustentáveis.
A biblioteca visa minimizar suas dependências. Atualmente, são necessárias as seguintes dependências:
anyio : para operações de arquivos assíncronos e redes.async-lru : Para cache de assíncrono.markdown : Para análise de marcha. Use ruff para linha e formatação, mypy para análise de código estático e pytest para teste.
A documentação é construída com mkdocs-material e mkdocstrings .
Todas as contribuições são bem -vindas, incluindo mais documentação, exemplos, código e testes. Até perguntas.
O pacote é de código aberto sob as condições da licença do MIT.