Code source : https://github.com/volfpeter/htmy
Documentation et exemples : https://volfpeter.github.io/htmy
htmyAsync , moteur de rendu pur-python .
ErrorBoundary intégré, facile à utiliser pour une gestion gracieuse des erreurs.Le package est disponible sur PYPI et peut être installé avec:
$ pip install htmy L'ensemble de la bibliothèque - du moteur de rendu lui-même aux composants intégrés - est construit autour de quelques protocoles simples et une poignée de classes de services publics simples. Cela signifie que vous pouvez facilement personnaliser, étendre ou remplacer essentiellement tout dans la bibliothèque. Oui, même le moteur de rendu. Les pièces restantes continueront de fonctionner comme prévu.
De plus, la bibliothèque ne s'appuie pas sur des fonctionnalités avancées de Python telles que les métaclases ou les descripteurs. Il n'y a pas non plus de classes de base complexes et autres. Même un ingénieur junior pouvait comprendre, développer et déboguer une application construite avec htmy .
Chaque classe avec une synchronisation ou un Async htmy(context: Context) -> Component est un composant htmy (techniquement un HTMYComponentType ). Les chaînes sont également des composants, ainsi que des listes ou des tuples d'objets HTMYComponentType ou String.
L'utilisation de ce nom de méthode permet la conversion de l'un de vos objets commerciaux (des modèles TypedDicts ou pydantic aux classes ORM) en composants sans craindre la collision de noms avec d'autres outils.
La prise en charge asynchrone permet de charger des données ou d'exécuter la logique commerciale asynchrone dans vos composants. Cela peut réduire la quantité de passe-partout que vous devez écrire dans certains cas, et vous donne également la liberté de diviser la logique de rendu et de non-renvoi de toutes les manières qui vous conviennent.
Exemple:
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 fournit également un décorateur @component qui peut être utilisé sur Sync ou Async my_component(props: MyProps, context: Context) -> Component pour les convertir en composants (préservant le typage props ).
Voici le même exemple que ci-dessus, mais avec des composants de fonction:
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 possède un riche ensemble d'utilitaires et de composants intégrés pour HTML et d'autres cas d'utilisation:
html : un ensemble complet de balises HTML de base.md : Utilitaire MarkdownParser et composant MD pour le chargement, l'analyse, la conversion et le contenu de marque.i18n : Utilitaires pour Async, Internationalisation basée sur JSON.BaseTag , TagWithProps , Tag , WildcardTag : Classes de base pour les balises XML personnalisées.ErrorBoundary , Fragment , SafeStr , WithContext : Utilitaires pour la gestion des erreurs, les emballages de composants, les fournisseurs de contexte et le formatage.Snippet : classe utilitaire pour charger et personnaliser les extraits de documents du système de fichiers.etree.ETreeConverter : utilitaire qui convertit XML en un arbre de composant avec prise en charge des composants HTMY personnalisés. htmy.HTMY est le rendu par défaut intégré de la bibliothèque.
Si vous utilisez la bibliothèque dans un framework Web asynchronisé comme Fastapi, alors vous êtes déjà dans un environnement asynchrone, afin que vous puissiez rendre des composants aussi simplement que celui-ci: await HTMY().render(my_root_component) .
Si vous essayez d'exécuter le rendu dans un environnement de synchronisation, comme un script local ou une CLI, vous devez d'abord envelopper le rendu dans une tâche asynchrone et exécuter cette tâche avec 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 ()) Comme vous pouvez le voir dans les exemples de code ci-dessus, chaque composant a un argument context: Context , que nous n'avons pas utilisé jusqu'à présent. Le contexte est un moyen de partager des données avec l'ensemble du sous-arbre d'un composant sans "forage d'hélice".
Le contexte (techniquement une Mapping ) est entièrement géré par le rendu. Composants du fournisseur de contexte (Toute classe avec une synchronisation ou une méthode de synchronisation Async htmy_context() -> Context ) Ajoutez de nouvelles données au contexte pour la rendre disponible aux composants de leur sous-arbre, et les composants peuvent simplement prendre ce dont ils ont besoin dans le contexte.
Il n'y a aucune restriction sur ce qui peut être dans le contexte, il peut être utilisé pour tout ce dont l'application a besoin, par exemple, mettant l'utilisateur actuel, les préférences, les thèmes ou les formateurs disponibles pour les composants. En fait, les composants intégrés tirent leur Formatter à partir du contexte s'il en contient un, pour permettre de personnaliser le nom de la propriété et le formatage de la valeur de la balise.
Voici un exemple de fournisseur de contexte et de mise en œuvre des consommateurs:
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 ()) Vous pouvez bien sûr compter sur les utilitaires liés au contexte intégrés comme les classes ContextAware ou WithContext pour une utilisation de contexte pratique et tapée avec un code moins bulleux.
Comme mentionné précédemment, la classe Formatter intégrée est responsable du nom d'attribut de balise et du formatage de valeur. Vous pouvez complètement remplacer ou étendre le comportement de mise en forme intégré simplement en étendant cette classe ou en ajoutant de nouvelles règles à une instance de celui-ci, puis en ajoutant l'instance personnalisée au contexte, soit directement dans HTMY ou HTMY.render() , soit dans un composant de fournisseur de contexte.
Ce sont des règles de formatage d'attribut de balise par défaut:
_ -> - ) à moins que le nom d'attribut ne démarre ou se termine par un soulignement, auquel cas les soulignements de direction et de fuite sont supprimés et le reste du nom d'attribut est préservé. Par exemple, data_theme="dark" est converti en data-theme="dark" , mais _data_theme="dark" finira par data_theme="dark" dans le texte rendu. Plus important encore, class_="text-danger" , _class="text-danger" , _class__="text-danger" sont toutes converties en class="text-danger" , et _for="my-input" ou for_="my_input" deviendra for="my-input" .bool sont converties en chaînes ( "true" et "false" ).XBool.true sont converties en une chaîne vide et les valeurs XBool.false sont ignorées (seul le nom d'attribut est rendu).date et datetime sont converties en chaînes ISO. Le composant ErrorBoundary est utile si vous souhaitez que votre application échoue gracieusement (par exemple, affichez un message d'erreur) au lieu d'augmenter une erreur HTTP.
La limite d'erreur enroule un sous-arbre du composant de composant. Lorsque le rendu rencontre un composant ErrorBoundary , il essaiera de rendre son contenu enveloppé. Si le rendu échoue avec une exception à tout moment du sous-arbre de ErrorBoundary , le rendu se repliera automatiquement au composant que vous avez attribué à la propriété fallback ErrorBoundary .
Facultativement, vous pouvez définir les erreurs qu'une limite d'erreur peut gérer, vous donnant un bon contrôle sur la gestion des erreurs.
En général, un composant doit être asynchronisé s'il doit attendre un appel asynchrone à l'intérieur.
Si un composant exécute un appel synchrone potentiellement "à long terme", il est fortement recommandé de déléguer cet appel à un fil de travail et l'attendre (ce qui rend le composant asynchronisé). Cela peut être fait par exemple avec l'utilitaire to_thread de anyio , starlette (ou fastapi 's) run_in_threadpool() , etc. L'objectif ici est d'éviter de bloquer la boucle d'événement Asyncio, car cela peut entraîner des problèmes de performances.
Dans tous les autres cas, il est préférable d'utiliser des composants de synchronisation.
Fastapi:
À une extrémité du spectre, il existe les frameworks d'application complets qui combinent les applications Server (Python) et Client (JavaScript) avec l'ensemble de la gestion de l'état et de la synchronisation dans un package Python (et dans certains cas un javascript supplémentaire). Certains des exemples les plus populaires sont: Reflex, Nicegui, Reactpy et FastUi.
Le principal avantage de ces cadres est un prototypage d'applications rapides et une expérience de développeur très pratique (au moins tant que vous restez dans l'ensemble de fonctionnalités intégré du cadre). En échange de cela, ils sont très avisés (des composants aux outils frontaux et à la gestion de l'État), l'ingénierie sous-jacente est très complexe, le déploiement et la mise à l'échelle peuvent être difficiles ou coûteux, et ils peuvent être difficiles à migrer. Même avec ces mises en garde, elles peuvent être un très bon choix pour les outils internes et le prototypage d'applications.
L'autre extrémité du spectre - moteurs de rendu simple - est dominé par le moteur de modèles Jinja, qui est un choix sûr comme il l'a été et sera là depuis longtemps. Les principaux inconvénients avec Jinja sont le manque de bon support IDE, le manque complet de support d'analyse de code statique et la syntaxe laide (subjectivement).
Ensuite, il existe des outils qui visent le milieu du milieu, généralement en offrant la plupart des avantages et des inconvénients des cadres d'application complets tout en laissant la gestion de l'État, la communication client-serveur et les mises à jour dynamiques de l'interface utilisateur pour que l'utilisateur puisse résoudre, souvent avec un certain niveau de support HTMX. Ce groupe comprend des bibliothèques comme FasthTML et Ludic.
L'objectif principal de htmy est d'être un moteur de rendu asynchronisé et pur-python, qui est aussi simple , maintenable et personnalisable que possible, tout en fournissant tous les éléments constitutifs pour des applications complexes et maintenables créatives.
La bibliothèque vise à miniser ses dépendances. Actuellement, les dépendances suivantes sont requises:
anyio : pour les opérations de fichiers asynchrones et le réseautage.async-lru : pour la mise en cache asynchrone.markdown : Pour l'analyse de Markdown. Utilisez ruff pour la libellur et le formatage, mypy pour l'analyse de code statique et pytest pour les tests.
La documentation est construite avec mkdocs-material et mkdocstrings .
Toutes les contributions sont les bienvenues, y compris plus de documentation, d'exemples, de code et de tests. Même des questions.
Le package est open source dans les conditions de la licence MIT.