Quellcode : https://github.com/volfpeter/htmy
Dokumentation und Beispiele : https://volfpeter.github.io/htmy
htmyAsync , Pure-Python- Rendering-Motor.
ErrorBoundary Komponente für die anmutige Fehlerbehandlung.Das Paket ist auf PYPI erhältlich und kann installiert werden mit:
$ pip install htmy Die gesamte Bibliothek-von der Rendering-Engine selbst bis zu den eingebauten Komponenten-basiert auf ein paar einfachen Protokollen und einer Handvoll einfacher Versorgungsklassen. Dies bedeutet, dass Sie alles in der Bibliothek problemlos anpassen, erweitern oder ersetzen können. Ja, sogar der Rendering -Motor. Die verbleibenden Teile funktionieren wie erwartet weiter.
Außerdem beruht die Bibliothek nicht auf fortschrittliche Python -Funktionen wie Metaklasse oder Deskriptoren. Es gibt auch keine komplexen Basisklassen und dergleichen. Sogar ein Junior -Ingenieur konnte eine Anwendung, die mit htmy aufgebaut wurde, verstehen, entwickeln und debuggen.
Jede Klasse mit einer Synchronisation oder einer asynchronen htmy(context: Context) -> Component ist eine htmy -Komponente (technisch gesehen ein HTMYComponentType ). Saiten sind auch Komponenten sowie Listen oder Tupel von HTMYComponentType oder String -Objekten.
Mit diesem Methodennamen können Sie Ihre Geschäftsobjekte (von TypedDicts s oder pydantic Modellen in ORM -Klassen) in Komponenten ohne Angst vor der Kollision mit anderen Werkzeugen umwandeln.
ASYNC -Unterstützung ermöglicht es, Daten zu laden oder eine async -Geschäftslogik direkt in Ihren Komponenten auszuführen. Dies kann die Menge an Kesselplatten verringern, die Sie in einigen Fällen schreiben müssen, und gibt Ihnen auch die Freiheit, die Rendering- und Nichtrender-Logik auf eine Weise zu teilen, die Sie für richtig halten.
Beispiel:
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 bietet auch einen @component Decorator an, der für Sync- oder Async my_component(props: MyProps, context: Context) -> Component verwendet werden können, um sie in Komponenten umzuwandeln (die props -Tipps).
Hier ist das gleiche Beispiel wie oben, jedoch mit Funktionskomponenten:
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 verfügt über eine Menge integrierter Versorgungsunternehmen und Komponenten sowohl für HTML als auch für andere Anwendungsfälle:
html -Modul: Ein vollständiger Satz von Basis -HTML -Tags.md : MarkdownParser -Dienstprogramm- und MD -Komponente zum Laden, Parsen, Konvertieren und Rendern von Markdown -Inhalten.i18n : Dienstprogramme für Async, JSON -basierte Internationalisierung.BaseTag , TagWithProps , Tag , WildcardTag : Basisklassen für benutzerdefinierte XML -Tags.ErrorBoundary , Fragment , SafeStr , WithContext : Dienstprogramme für Fehlerbehandlung, Komponentenverpackungen, Kontextanbieter und Formatierung.Snippet : Dienstprogrammklasse zum Laden und Anpassen von Dokumentenausschnitten aus dem Dateisystem.etree.ETreeConverter : Dienstprogramm, das XML in einen Komponentenbaum umwandelt, wobei benutzerdefinierte HTMY -Komponenten unterstützt werden. htmy.HTMY ist der integrierte Standard-Renderer der Bibliothek.
Wenn Sie die Bibliothek in einem asynchronen Web -Framework wie Fastapi verwenden, befinden Sie sich bereits in einer asynchronen Umgebung, sodass Sie Komponenten einfach als folgt rendern können: await HTMY().render(my_root_component) .
Wenn Sie versuchen, den Renderer in einer SYNC -Umgebung wie ein lokales Skript oder eine CLI auszuführen, müssen Sie den Renderer zunächst in eine asynchronisierende Aufgabe einwickeln und diese Aufgabe mit asyncio.run() ausführen:
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 ()) Wie Sie aus den obigen Codebeispielen sehen konnten, hat jede Komponente einen context: Context Kontextargument, das wir bisher noch nicht verwendet haben. Der Kontext ist eine Möglichkeit, Daten mit dem gesamten Teilbaum einer Komponente ohne "Prop -Bohrung" zu teilen.
Der Kontext (technisch gesehen eine Mapping ) wird vom Renderer vollständig verwaltet. Kontextanbieterkomponenten (jede Klasse mit einer Synchronisation oder ASync htmy_context() -> Context ) fügen dem Kontext neue Daten hinzu, um den Komponenten in ihrem Unterbaum zur Verfügung zu stellen, und Komponenten können einfach das, was sie benötigen, aus dem Kontext übernehmen.
Es gibt keine Einschränkung in Bezug auf das, was im Kontext enthalten sein kann. Sie kann für alles verwendet werden, was die Anwendung benötigt, z. In der Tat erhalten integrierte Komponenten ihre Formatter aus dem Kontext, wenn es eine enthält, um das Anpassen von Tag-Eigenschaftsnamen und Wertformatierung des Tags zu ermöglichen.
Hier ist ein Beispiel -Kontextanbieter und Verbraucherimplementierung:
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 ()) Sie können sich natürlich auf die integrierten kontextbezogenen Dienstprogramme wie den ContextAware oder WithContext -Klassen für einen bequemen und typisierten Kontext mit weniger Boilerplate-Code verlassen.
Wie bereits erwähnt, ist die integrierte Formatter für Tag-Attributname und Wertformatierung verantwortlich. Sie können das eingebaute Formatierungsverhalten vollständig überschreiben oder erweitern, indem Sie diese Klasse einfach erweitern oder eine Instanz des IT hinzufügen und dann die benutzerdefinierte Instanz in den Kontext hinzufügen, entweder direkt in HTMY oder HTMY.render() oder in einer Kontextanbieterkomponente.
Dies sind Standard -Attribut -Formatierungsregeln für Tag -Tags:
_ -> - ), es sei denn, der Attributname startet oder endet mit einem Unterstrich. In diesem Fall bleibt der führende und nachverfolgende Unterstrich entfernt und der Rest des Attributnamens erhalten erhalten. Zum Beispiel wird data_theme="dark" in data-theme="dark" konvertiert, aber _data_theme="dark" wird im gerenderten Text als data_theme="dark" enden. Noch wichtiger ist, class_="text-danger" , _class="text-danger" , _class__="text-danger" werden alle in class="text-danger" konvertiert, und _for="my-input" oder for_="my_input" wird for="my-input" .bool -Attributwerte werden in Zeichenfolgen konvertiert ( "true" und "false" ).XBool.true -Attribute Werte werden in eine leere Zeichenfolge konvertiert, und XBool.false -Werte werden übersprungen (nur der Attributname wird gerendert).date und datetime -Attributwerte werden in ISO -Zeichenfolgen konvertiert. Die ErrorBoundary ist nützlich, wenn Ihre Anwendung ordnungsgemäß fehlschlägt (z. B. eine Fehlermeldung anzeigen), anstatt einen HTTP -Fehler zu erhöhen.
Die Fehlergrenze wickelt einen Komponentenkomponenten -Subtree. Wenn der Renderer auf eine ErrorBoundary trifft, wird versucht, seinen verpackten Inhalt zu rendern. Wenn das Rendering mit einer Ausnahme zu einem bestimmten Zeitpunkt im Subtree des ErrorBoundary fehlschlägt, fällt der Renderer automatisch auf die Komponente zurück, die Sie der fallback -Eigenschaft des ErrorBoundary zugewiesen haben.
Optional können Sie definieren, welche Fehler eine Fehlergrenze verarbeiten können, um eine feine Kontrolle über die Fehlerbehandlung zu erhalten.
Im Allgemeinen sollte eine Komponente asynchron sein, wenn sie auf einen asynchronen Aufruf innen warten muss.
Wenn eine Komponente einen potenziell "langlebigen" synchronen Anruf ausführt, wird dringend empfohlen, den Aufruf an einen Arbeiter-Thread zu delegieren (so dass die Komponente asynchronisiert wird). Dies kann beispielsweise mit anyio to_thread Utility, starlette (oder fastapi ) run_in_threadpool() usw. erfolgen. Das Ziel hier ist es, die Asyncio -Ereignisschleife zu vermeiden, da dies zu Leistungsproblemen führen kann.
In allen anderen Fällen ist es am besten, Sync -Komponenten zu verwenden.
Fastapi:
An einem Ende des Spektrums gibt es die vollständigen Anwendungs -Frameworks, die die Server- (Python-) und Client- (JavaScript) -Anwendungen mit der gesamten Statusverwaltung und -synchronisierung in ein einzelnes Python -Paket (in einigen Fällen ein zusätzliches JavaScript) -Paket kombinieren. Einige der beliebtesten Beispiele sind: Reflex, NiceGui, Reactpy und Fastui.
Der Hauptvorteil dieser Frameworks ist das schnelle Anwendungsprototyping und ein sehr bequemes Entwicklererlebnis (mindestens so lange, wie Sie innerhalb des integrierten Funktionssatzes des Frameworks bleiben). Im Austausch dafür sind sie sehr eindeutig (von Komponenten bis hin zu Frontend -Tooling und Staatsmanagement), das zugrunde liegende Engineering ist sehr komplex, einsatz und skaliert können schwierig oder kostspielig sein, und es kann schwer zu wandern sein. Selbst mit diesen Einschränkungen können sie eine sehr gute Wahl für interne Werkzeuge und Anwendungsprototypen sein.
Das andere Ende des Spektrums - einfache Rendering -Motoren - wird vom Jinja -Templating -Motor dominiert, was eine sichere Wahl ist, wie es sich lange Zeit getan hat und wird. Die Hauptnachteile mit Jinja sind der Mangel an guter IDE -Unterstützung, der vollständige Mangel an statischer Code -Analyseunterstützung und die (subjektiv) hässliche Syntax.
Anschließend gibt es Tools, die den Middleground anstreben, in der Regel, indem sie die meisten Vorteile und Nachteile vollständiger Anwendungsrahmen bieten und gleichzeitig das staatliche Management, die Kunden-Server-Kommunikation und die dynamischen UI-Updates für den Benutzer gelöst haben, häufig mit einem gewissen Grad an HTMX-Unterstützung. Diese Gruppe umfasst Bibliotheken wie Fasthtml und Ludic.
Das Hauptziel von htmy ist es, eine asynchronisierte , reine Python-Rendering-Engine zu sein, die so einfach , wartbar und anpassbar wie möglich ist und gleichzeitig alle Bausteine für (günstige) erstellen komplexe und wartbare Anwendungen bereitstellt.
Die Bibliothek zielt darauf ab, ihre Abhängigkeiten zu minimieren. Derzeit sind die folgenden Abhängigkeiten erforderlich:
anyio : Für asynchronisierte Dateioperationen und Networking.async-lru : Für asynchrones Caching.markdown : Für Markdown -Parsen. Verwenden Sie ruff zum Linken und Formatieren, mypy für die statische Codeanalyse und pytest zum Testen.
Die Dokumentation wird mit mkdocs-material und mkdocstrings gebaut.
Alle Beiträge sind willkommen, einschließlich weiterer Dokumentation, Beispiele, Code und Tests. Sogar Fragen.
Das Paket ist unter den Bedingungen der MIT-Lizenz offen.