From 91dee6e6852c5fc64a533992a44a1b49843f4fa1 Mon Sep 17 00:00:00 2001
From: Andreas Pelme htpy was heavily inspired by many other libraries and articles. This page lists some of them. htpy was heavily inspired by many other libraries that came before it. htpy is a library that makes writing HTML in plain Python fun and efficient, without a template language. Define HTML in Python:
+
References¶
-Projects that use htpy¶
+
+
Similar libraries and tools¶
+
from htpy import body, h1, head, html, li, title, ul\n\nmenu = [\"egg+bacon\", \"bacon+spam\", \"eggs+spam\"]\n\nprint(\n html[\n head[title[\"Todays menu\"]],\n body[\n h1[\"Menu\"],\n ul(\".menu\")[(li[item] for item in menu)],\n ],\n ]\n)\n
And get HTML:
<!DOCTYPE html>\n<html>\n <head>\n <title>Todays menu</title>\n </head>\n <body>\n <h1>Menu</h1>\n <ul class=\"menu\">\n <li>egg+bacon</li>\n <li>bacon+spam</li>\n <li>eggs+spam</li>\n </ul>\n </body>\n</html>\n
"},{"location":"#motivation-for-this-project","title":"Motivation for this project","text":"At Personalkollen, where htpy was originally developed we often found ourselves hitting walls when using classic templates. htpy was created to improve the productiveness and experience of generating HTML from a Python backend.
"},{"location":"#key-features","title":"Key features","text":"Leverage static types: Use mypy or pyright to type check your code.
Great debugging: Avoid cryptic stack traces from templates. Use your favorite Python debugger.
Easy to extend: There is no special way to define template tags/filters. Just call regular functions.
Works with existing Python web framework: Works great with Django, Flask or any other Python web framework!
Works great with htmx: htpy makes for a great experience when writing server rendered partials/components.
Create reusable components: Define components, snippets, complex layouts/pages as regular Python variables or functions.
Familiar concepts from React: React helped make it popular writing HTML with a programming language. htpy uses a lot of similar constructs.
htpy generates HTML elements and attributes and provide a few helpers.
htpy does not enforce any particular pattern or style to organize your pages, components and layouts. That does not mean that htpy cannot be used to build sophisticated web pages or applications.
Rather the opposite: you are encouraged the leverage the power of Python to structure your project. Use modules, classes, functions, decorators, list comprehension, generators, conditionals, static typing and any other feature of Python to organize your components. This gives you a lot of power and makes htpy scale from a single small Flask project to bigger applications.
Common patterns can give you some ideas that you can build upon yourself.
"},{"location":"#the-syntax","title":"The syntax","text":"Child elements are specified using the []
syntax. This may look strange at first but it has some nice benefits. This clearly separates attributes from children elements and makes the code more readable. It is implemented using the __getitem__
method, just like lists or dicts.
htpy is available on PyPI. You may install the latest version using pip:
pip install htpy\n
"},{"location":"#documentation","title":"Documentation","text":"The full documentation is available at https://htpy.dev: - Usage - Common patterns - Static typing - Usage with Django - Streaming of contents - FAQ - References
"},{"location":"common-patterns/","title":"Common patterns","text":"htpy itself is a library that does not impose any particular structure for your code. You have the full power of Python functions, classes and modules at your disposal.
General programming practices on how to structure modules, functions and classes apply to HTML generation with htpy.
This page describes common scenarios and patterns that may help you structure your own project in a good way.
"},{"location":"common-patterns/#filemodule-structure","title":"File/module structure","text":"It is generally a good idea to keep your HTML pages/components separate from HTTP request handling and \"business logic\".
In Django, this means that the view function should not directly generate the HTML.
Using a file named components.py
can be a good idea. If you have many components, you may create a components
package instead.
Your component functions can accept arbitrary argument with the required data. It is a good idea to only use keyword arguments (put a *
in the argument list to force keyword arguments):
from django.http import HttpRequest, HttpResponse\n\nfrom .components import greeting_page\n\ndef greeting(request: HttpRequest) -> HttpResponse:\n return HttpResponse(greeting_page(\n name=request.GET.get(\"name\", \"anonymous\"),\n ))\n
components.pyfrom htpy import html, body, h1\n\ndef greeting_page(*, name: str) -> Element:\n return html[body[h1[f\"hi {name}!\"]]]\n
"},{"location":"common-patterns/#using-a-base-layout","title":"Using a base layout","text":"A common feature of template languages is to \"extend\" a base/parent template and specify placeholders. This can be achieved with a base_layout
function:
import datetime\n\nfrom htpy import body, div, h1, head, html, p, title, Node, Element\n\n\ndef base_page(*,\n page_title: str | None = None,\n extra_head: Node = None,\n content: Node = None,\n body_class: str | None = None,\n) -> Element:\n return html[\n head[title[page_title], extra_head],\n body(class_=body_class)[\n content,\n div(\"#footer\")[f\"Copyright {datetime.date.today().year} by Foo Inc.\"],\n ],\n ]\n\n\ndef index_page() -> Element:\n return base_page(\n page_title=\"Welcome!\",\n body_class=\"green\",\n content=[\n h1[\"Welcome to my site!\"],\n p[\"Hello and welcome!\"],\n ],\n )\n\n\ndef about_page() -> Element:\n return base_page(\n page_title=\"About us\",\n content=[\n h1[\"About us\"],\n p[\"We love creating web sites!\"],\n ],\n )\n
"},{"location":"common-patterns/#ui-components","title":"UI components","text":"Creating higher level wrappers for common UI components can be a good idea to reduce repetition.
Wrapping Bootstrap Modal could be achieved with a function like this:
Creating wrapper for Bootstrap Modalfrom markupsafe import Markup\n\nfrom htpy import Element, Node, button, div, h5, span\n\n\ndef bootstrap_modal(*, title: str, body: Node = None, footer: Node = None) -> Element:\n return div(\".modal\", tabindex=\"-1\", role=\"dialog\")[\n div(\".modal-dialog\", role=\"document\")[\n div(\".modal-content\")[\n div(\".modal-header\")[\n div(\".modal-title\")[\n h5(\".modal-title\")[title],\n button(\n \".close\",\n type=\"button\",\n data_dismiss=\"modal\",\n aria_label=\"Close\",\n )[span(aria_hidden=\"true\")[Markup(\"×\")]],\n ]\n ],\n div(\".modal-body\")[body],\n footer and div(\".modal-footer\")[footer],\n ]\n ]\n ]\n
You would then use it like this:
from htpy import button, p\n\nprint(\n bootstrap_modal(\n title=\"Modal title\",\n body=p[\"Modal body text goes here.\"],\n footer=[\n button(\".btn.btn-primary\", type=\"button\")[\"Save changes\"],\n button(\".btn.btn-secondary\", type=\"button\")[\"Close\"],\n ],\n )\n)\n
"},{"location":"django/","title":"Usage with Django","text":"htpy is not tied to any specific web framework. Nonetheless, htpy works great when combined with Django. This page contains information and useful techniques on how to combine htpy and Django.
"},{"location":"django/#returning-a-htpy-response","title":"Returning a htpy response","text":"htpy elements can be passed directly to HttpResponse
:
from django.http import HttpResponse\nfrom htpy import html, body, div\n\ndef my_view(request):\n return HttpResponse(html[body[div[\"Hi Django!\"]]])\n
"},{"location":"django/#using-htpy-as-part-of-an-existing-django-template","title":"Using htpy as part of an existing Django template","text":"htpy elements are marked as \"safe\" and can be injected directly into Django templates. This can be useful if you want to start using htpy gradually in an existing template based Django project:
base.html<html>\n <head>\n <title>My Django Site</title>\n </head>\n <body>{{ content }}</body>\n</html>\n
views.pyfrom django.shortcuts import render\n\nfrom htpy import h1\n\n\ndef index(request):\n return render(request, \"base.html\", {\n \"content\": h1[\"Welcome to my site!\"],\n })\n
"},{"location":"django/#render-a-django-form","title":"Render a Django form","text":"CSRF token, form widgets and errors can be directly used within htpy elements:
forms.pyfrom django import forms\n\n\nclass MyForm(forms.Form):\n name = forms.CharField()\n
views.pyfrom django.http import HttpRequest, HttpResponse\n\nfrom .components import my_form_page, my_form_success_page\nfrom .forms import MyForm\n\n\ndef my_form(request: HttpRequest) -> HttpResponse:\n form = MyForm(request.POST or None)\n if form.is_valid():\n return HttpResponse(my_form_success_page())\n\n return HttpResponse(my_form_page(request, my_form=form))\n
components.pyfrom django.http import HttpRequest\nfrom django.template.backends.utils import csrf_input\n\nfrom htpy import Element, Node, body, button, form, h1, head, html, title\n\nfrom .forms import MyForm\n\n\ndef base_page(page_title: str, content: Node) -> Element:\n return html[\n head[title[page_title]],\n body[content],\n ]\n\n\ndef my_form_page(request: HttpRequest, *, my_form: MyForm) -> Element:\n return base_page(\n \"My form\",\n form(method=\"post\")[\n csrf_input(request),\n my_form.errors,\n my_form[\"name\"],\n button[\"Submit!\"],\n ],\n )\n\n\ndef my_form_success_page() -> Element:\n return base_page(\n \"Success!\",\n h1[\"Success! The form was valid!\"],\n )\n
"},{"location":"django/#implement-custom-form-widgets-with-htpy","title":"Implement custom form widgets with htpy","text":"You can implement a custom form widget directly with htpy like this:
widgets.pyfrom django.forms import widgets\n\nfrom htpy import sl_input\n\n\nclass ShoelaceInput(widgets.Widget):\n \"\"\"\n A form widget using Shoelace's <sl-input> element.\n More info: https://shoelace.style/components/input\n \"\"\"\n\n def render(self, name, value, attrs=None, renderer=None):\n return str(sl_input(attrs, name=name, value=value))\n
"},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#how-does-htpy-performance-compare-to-django-or-jinja-templates","title":"How does htpy performance compare to Django or Jinja templates?","text":"The performance of HTML rendering is rarely the bottleneck in most web application. It is usually fast enough regardless of what method of constructing the HTML is being used.
Given that it has been fast enough, there has not been much effort in optimizing htpy. It should be possible to significantly increase the effectiveness and we are open to contributions with benchmarks and speed improvements.
That said, htpy is currently on par with Django templates when it comes to speed. Jinja2 is currently significantly faster than both Django templates and htpy. There is a small benchmark script in the repo that generates a table with 50 000 rows.
"},{"location":"faq/#can-htpy-generate-xmlxhtml","title":"Can htpy generate XML/XHTML?","text":"No. Generating XML/XHTML is out of scope for this project. Use a XML library if you are looking to generate XML.
htpy generates HTML, therefore \"void elements\" such as <br>
does not include a trailing /
.
If you are looking to generate generic XML, lxml.builder
could be a good alternative.
With a template language, putting HTML markup in separate files is enforced by design. Avoiding logic in the presentation layer is also mostly done by making the language very restrictive.
It takes a little bit of planning and effort, but it is possible to have a nicely separated presentation layer that is free from logic. See Common patterns for more details on how you can structure your project.
"},{"location":"faq/#what-kind-of-black-magic-makes-from-htpy-import-whatever_element-work","title":"What kind of black magic makesfrom htpy import whatever_element
work?","text":"htpy uses the module level __getattr__
. It was introduced in Python 3.7. It allows creating Element
instances for any elements that are imported.
htpy must be compatible with standard Python code formatters, editors and static type checkers. Unfortunately, it is not possible to support those workflows with a custom syntax without a massive effort to change those tools to support that syntax.
"},{"location":"references/","title":"References","text":"htpy was heavily inspired by many other libraries and articles. This page lists some of them.
"},{"location":"references/#similar-libraries-and-tools","title":"Similar libraries and tools","text":"[]
syntax for specifying children. Not actively maintained.[]
syntax for children. Not actively maintained.()
) for attributes and getitem ([]
) for children.E
allows creating XML/XHTML documents from Python.htpy was designed to be used with static typing. Since you define all your own data/components with regular Python, a static type checker like mypy will catch errors like this:
class User:\n def __init__(self, name: str):\n self.name = name\n\n\ndef greeting(user: User) -> Element:\n return h1[f\"Hi {user.first_name.capitalize()}!]\n# ^^^^^^^^^^\n# mypy: error: \"User\" has no attribute \"first_name\" [attr-defined]\n
"},{"location":"static-typing/#autocompletion-of-html-elements","title":"Autocompletion of HTML elements","text":"htpy ships with type annotations for all HTML elements. If your editor supports it, it will show you useful auto completions:
"},{"location":"static-typing/#element-and-voidelement-classes","title":"Element
and VoidElement
classes","text":"The base types/classes in htpy are Element
and VoidElement
. Element
are all regular HTML elements that can have children such as <div>
, <span>
and <table>
. VoidElement
are HTML void element which cannot have children such as <img>
, <input>
and <br>
.
Use Element
as the return type when you want to always return an element.
from typing import Literal\n\nfrom htpy import Element, span\n\n\ndef bootstrap_badge(\n text: str,\n style: Literal[\"primary\", \"success\", \"danger\"] = \"primary\",\n) -> Element:\n return span(f\".badge.text-bg-{style}\")[text]\n
"},{"location":"static-typing/#node","title":"Node","text":"Node
is a type alias for all possible objects that can be used as a child node. See the source for the exact definition that defines all kinds of nodes that can be children of an element. This is a wider type than Element
since child nodes can be str, markup, None, iterables or callables.
Use Node
when you want to create a wrapper function to be flexible with what you accept. This function will accept both a str or some other element to be passed as contents
:
from htpy import Element, Node, div\n\ndef bootstrap_alert(contents: Node) -> Element:\n return div(\".alert\", role=\"alert\")[contents]\n
"},{"location":"streaming/","title":"Streaming of contents","text":"Internally, htpy is built with generators. Most of the time, you would render the full page with str()
, but htpy can also incrementally generate pages which can then be streamed to the browser. If your page uses a database or other services to retrieve data, you can sending the first part of the page to the client while the page is being generated.
Note
Streaming requires a bit of discipline and care to get right. You need to ensure to avoid doing too much work up front and use lazy constructs such as generators and callables. Most of the time, rendering the page without streaming will be the easiest way to get going. Streaming can give you improved user experience from faster pages/rendering.
This video shows what it looks like in the browser to generate a HTML table with Django StreamingHttpResponse (source code):
This example simulates a (very) slow fetch of data and shows the power of streaming: The browser loads CSS and gradually shows the contents. By loading CSS files in the <head>
tag before dynamic content, the browser can start working on loading the CSS and styling the page while the server keeps generating the rest of the page.
Django's querysets are lazily evaluated. They will not execute a database query before their value is actually needed.
This example shows how this property of Django querysets can be used to create a page that streams objects:
from django.http import StreamingHttpResponse\nfrom htpy import ul, li\n\nfrom myapp.models import Article\n\ndef article_list(request):\n return StreamingHttpResponse(ul[\n (li[article.title] for article in Article.objects.all())\n ])\n
"},{"location":"streaming/#using-callables-to-delay-evalutation","title":"Using callables to delay evalutation","text":"Pass a callable that does not accept any arguements as child to delay the evaluation.
This example shows how the page starts rendering and outputs the <h1>
tag and then calls calculate_magic_number
.
import time\nfrom htpy import div, h1\n\ndef calculate_magic_number() -> str:\n time.sleep(1)\n print(\" (running the complex calculation...)\")\n return \"42\"\n\nelement = div[\n h1[\"Welcome to my page\"],\n \"The magic number is \",\n calculate_magic_number,\n]\n\nfor chunk in element:\n print(chunk)\n
Output:
<div>\n<h1>\nWelcome to my page\n</h1>\nThe magic number is\n42 # <-- Appears after 3 seconds\n</div>\n
You may use lambda
to create a function without arguments to make a an expression lazy:
from htpy import div, h1\n\n\ndef fib(n: int) -> int:\n if n == 0:\n return 0\n elif n == 1:\n return 1\n else:\n return fib(n - 1) + fib(n - 2)\n\n\nprint(\n div[\n h1[\"Fibonacci!\"],\n \"fib(20)=\",\n lambda: str(fib(20)),\n ]\n)\n# output: <div><h1>Fibonacci!</h1>fib(12)=6765</div>\n
"},{"location":"usage/","title":"Usage","text":"Elements are imported directly from the htpy
module as their name. HTML attributes are specified by parenthesis (()
/ \"call\"). Children are specified using square brackets ([]
/ \"getitem\").
>>> from htpy import div\n>>> print(div(id=\"hi\")[\"Hello!\"])\n<div id=\"hi\">Hello!</div>\n
"},{"location":"usage/#elements","title":"Elements","text":"Children can be strings, markup, other elements or lists/iterators.
Elements can be arbitrarily nested: Nested elements
>>> from htpy import article, section, p\n>>> print(section[article[p[\"Lorem ipsum\"]]])\n<section><article><p>Lorem ipsum</p></article></section>\n
"},{"location":"usage/#textstrings","title":"Text/strings","text":"It is possible to pass a string directly: Using a string as children
>>> from htpy import h1\n>>> print(h1[\"Welcome to my site!\"])\n<h1>Welcome to my site!</h1>\n
Strings are automatically escaped to avoid XSS vulnerabilities. It is convenient and safe to directly insert variable data via f-strings:
>>> from htpy import h1\n>>> user_supplied_name = \"bobby </h1>\"\n>>> print(h1[f\"hello {user_supplied_name}\"])\n<h1>hello bobby </h1></h1>\n
"},{"location":"usage/#conditional-rendering","title":"Conditional rendering","text":"None
will not render anything. This can be useful to conditionally render some content.
>>> from htpy import div, b\n>>> error = None\n\n>>> # No <b> tag will be rendered since error is None\n>>> print(div[error and b[error]])\n<div></div>\n\n>>> error = 'Enter a valid email address.'\n>>> print(div[error and b[error]])\n<div><b>Enter a valid email address.</b></div>\n\n# Inline if/else can also be used:\n>>> print(div[b[error] if error else None])\n<div><b>Enter a valid email address.</b></div>\n
"},{"location":"usage/#loops-iterating-over-children","title":"Loops / iterating over children","text":"You can pass a list, tuple or generator to generate multiple children:
Iterate over a generator>>> from htpy import ul, li\n>>> print(ul[(li[letter] for letter in \"abc\")])\n<ul><li>a</li><li>b</li><li>c</li></ul>\n
Note
The generator will be lazily evaluated when rendering the element, not directly when the element is constructed. See Streaming for more information.
A list
can be used similar to a JSX fragment:
>>> from htpy import div, img\n>>> my_images = [img(src=\"a.jpg\"), img(src=\"b.jpg\")]\n>>> print(div[my_images])\n<div><img src=\"a.jpg\"><img src=\"b.jpg\"></div>\n
"},{"location":"usage/#custom-elements-web-components","title":"Custom elements / web components","text":"Custom elements / web components are HTML elements that contains at least one dash (-
). Since -
cannot be used in Python identifiers, use underscore (_
) instead:
>>> from htpy import my_custom_element\n>>> print(my_custom_element['hi!'])\n<my-custom-element>hi!</my-custom-element>\n
"},{"location":"usage/#injecting-markup","title":"Injecting markup","text":"If you have HTML markup that you want to insert without further escaping, wrap it in Markup
from the markupsafe library. markupsafe is a dependency of htpy and is automatically installed:
>>> from htpy import div\n>>> from markupsafe import Markup\n>>> print(div[Markup(\"<foo></foo>\")])\n<div><foo></foo></div>\n
If you are generate Markdown and want to insert it into an element, use Markup
: Injecting generated markdown
>>> from markdown import markdown\n>>> from markupsafe import Markup\n>>> from htpy import div\n>>> print(div[Markup(markdown('# Hi'))])\n<div><h1>Hi</h1></div>\n
"},{"location":"usage/#html-doctype","title":"HTML Doctype","text":"The HTML doctype is automatically prepended to the <html>
tag:
>>> from htpy import html\n>>> print(html)\n<!doctype html><html></html>\n
"},{"location":"usage/#attributes","title":"Attributes","text":"HTML attributes are defined by calling the element. They can be specified in a couple of different ways.
"},{"location":"usage/#elements-without-attributes","title":"Elements without attributes","text":"Some elements do not have attributes, they can be specified by just the element itself:
>>> from htpy import hr\n>>> print(hr)\n<hr>\n
"},{"location":"usage/#keyword-arguments","title":"Keyword arguments","text":"Attributes can be specified via keyword arguments:
>>> from htpy import img\n>>> print(img(src=\"picture.jpg\"))\n<img src=\"picture.jpg\">\n
In Python, class
and for
cannot be used as keyword arguments. Instead, they can be specified as class_
or for_
when using keyword arguments:
>>> from htpy import label\n>>> print(label(for_=\"myfield\"))\n<label for=\"myfield\"></label>\n
Attributes that contains dashes -
can be specified using underscores:
>>> from htpy import form\n>>> print(form(hx_post=\"/foo\"))\n<form hx-post=\"/foo\"></form>\n
"},{"location":"usage/#idclass-shorthand","title":"id/class shorthand","text":"Defining id
and class
attributes is common when writing HTML. A string shorthand that looks like a CSS selector can be used to quickly define id and classes:
>>> from htpy import div\n>>> print(div(\"#myid\"))\n<div id=\"myid\"></div>\n
Define multiple classes>>> from htpy import div\n>>> print(div(\".foo.bar\"))\n<div id=\"foo bar\"></div>\n
Combining both id and classes>>> from htpy import div\n>>> print(div(\"#myid.foo.bar\"))\n<div id=\"myid\" class=\"foo bar\"></div>\n
"},{"location":"usage/#attributes-as-dict","title":"Attributes as dict","text":"Attributes can also be specified as a dict
. This is useful when using attributes that are reserved Python keywords (like for
or class
), when the attribute name contains a dash (-
) or when you want to define attributes dynamically.
>>> from htpy import div\n>>> print(div({\"data-foo\": \"bar\"}))\n<div data-foo=\"bar\"></div>\n
Using an attribute with a reserved keyword>>> from htpy import label\n>>> print(label({\"for\": \"myfield\"}))\n<label for=\"myfield\"></label>\n
"},{"location":"usage/#boolean-attributes","title":"Boolean attributes","text":"In HTML, boolean attributes such as disabled
are considered \"true\" when they exist. Specifying an attribute as True
will make it appear (without a value). False
will make it hidden. This is useful and brings the semantics of bool
to HTML.
>>> from htpy import button\n>>> print(button(disabled=True))\n<button disabled></button>\n
False bool attribute>>> from htpy import button\n>>> print(button(disabled=False))\n<button></button>\n
"},{"location":"usage/#conditionally-mixing-css-classes","title":"Conditionally mixing CSS classes","text":"To make it easier to mix CSS classes, the class
attribute accepts a list of class names or a dict. Falsey values will be ignored.
>>> from htpy import button\n>>> is_primary = True\n>>> print(button(class_=[\"btn\", {\"btn-primary\": is_primary}]))\n<button class=\"btn btn-primary\"></button>\n>>> is_primary = False\n>>> print(button(class_=[\"btn\", {\"btn-primary\": is_primary}]))\n<button class=\"btn\"></button>\n>>>\n
"},{"location":"usage/#combining-modes","title":"Combining modes","text":"Attributes via id/class shorthand, keyword arguments and dictionary can be combined:
Specifying attribute via multiple arguments>>> from htyp import label\n>>> print(label(\"#myid.foo.bar\", {'for': \"somefield\"}, name=\"myname\",))\n<label id=\"myid\" class=\"foo bar\" for=\"somefield\" name=\"myname\"></label>\n
"},{"location":"usage/#iterating-of-the-output","title":"Iterating of the output","text":"Iterating over a htpy element will yield the resulting contents in chunks as they are rendered:
>>> from htpy import ul, li\n>>> for chunk in ul[li[\"a\", \"b\"]]:\n... print(f\"got a chunk: {chunk!r}\")\n...\ngot a chunk: '<ul>'\ngot a chunk: '<li>'\ngot a chunk: 'a'\ngot a chunk: 'b'\ngot a chunk: '</li>'\ngot a chunk: '</ul>'\n
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":""},{"location":"#htpy-html-in-python","title":"htpy - HTML in Python","text":"htpy is a library that makes writing HTML in plain Python fun and efficient, without a template language.
Define HTML in Python:
from htpy import body, h1, head, html, li, title, ul\n\nmenu = [\"egg+bacon\", \"bacon+spam\", \"eggs+spam\"]\n\nprint(\n html[\n head[title[\"Todays menu\"]],\n body[\n h1[\"Menu\"],\n ul(\".menu\")[(li[item] for item in menu)],\n ],\n ]\n)\n
And get HTML:
<!DOCTYPE html>\n<html>\n <head>\n <title>Todays menu</title>\n </head>\n <body>\n <h1>Menu</h1>\n <ul class=\"menu\">\n <li>egg+bacon</li>\n <li>bacon+spam</li>\n <li>eggs+spam</li>\n </ul>\n </body>\n</html>\n
"},{"location":"#motivation-for-this-project","title":"Motivation for this project","text":"At Personalkollen, where htpy was originally developed we often found ourselves hitting walls when using classic templates. htpy was created to improve the productiveness and experience of generating HTML from a Python backend.
"},{"location":"#key-features","title":"Key features","text":"Leverage static types: Use mypy or pyright to type check your code.
Great debugging: Avoid cryptic stack traces from templates. Use your favorite Python debugger.
Easy to extend: There is no special way to define template tags/filters. Just call regular functions.
Works with existing Python web framework: Works great with Django, Flask or any other Python web framework!
Works great with htmx: htpy makes for a great experience when writing server rendered partials/components.
Create reusable components: Define components, snippets, complex layouts/pages as regular Python variables or functions.
Familiar concepts from React: React helped make it popular writing HTML with a programming language. htpy uses a lot of similar constructs.
htpy generates HTML elements and attributes and provide a few helpers.
htpy does not enforce any particular pattern or style to organize your pages, components and layouts. That does not mean that htpy cannot be used to build sophisticated web pages or applications.
Rather the opposite: you are encouraged the leverage the power of Python to structure your project. Use modules, classes, functions, decorators, list comprehension, generators, conditionals, static typing and any other feature of Python to organize your components. This gives you a lot of power and makes htpy scale from a single small Flask project to bigger applications.
Common patterns can give you some ideas that you can build upon yourself.
"},{"location":"#the-syntax","title":"The syntax","text":"Child elements are specified using the []
syntax. This may look strange at first but it has some nice benefits. This clearly separates attributes from children elements and makes the code more readable. It is implemented using the __getitem__
method, just like lists or dicts.
htpy is available on PyPI. You may install the latest version using pip:
pip install htpy\n
"},{"location":"#documentation","title":"Documentation","text":"The full documentation is available at https://htpy.dev: - Usage - Common patterns - Static typing - Usage with Django - Streaming of contents - FAQ - References
"},{"location":"common-patterns/","title":"Common patterns","text":"htpy itself is a library that does not impose any particular structure for your code. You have the full power of Python functions, classes and modules at your disposal.
General programming practices on how to structure modules, functions and classes apply to HTML generation with htpy.
This page describes common scenarios and patterns that may help you structure your own project in a good way.
"},{"location":"common-patterns/#filemodule-structure","title":"File/module structure","text":"It is generally a good idea to keep your HTML pages/components separate from HTTP request handling and \"business logic\".
In Django, this means that the view function should not directly generate the HTML.
Using a file named components.py
can be a good idea. If you have many components, you may create a components
package instead.
Your component functions can accept arbitrary argument with the required data. It is a good idea to only use keyword arguments (put a *
in the argument list to force keyword arguments):
from django.http import HttpRequest, HttpResponse\n\nfrom .components import greeting_page\n\ndef greeting(request: HttpRequest) -> HttpResponse:\n return HttpResponse(greeting_page(\n name=request.GET.get(\"name\", \"anonymous\"),\n ))\n
components.pyfrom htpy import html, body, h1\n\ndef greeting_page(*, name: str) -> Element:\n return html[body[h1[f\"hi {name}!\"]]]\n
"},{"location":"common-patterns/#using-a-base-layout","title":"Using a base layout","text":"A common feature of template languages is to \"extend\" a base/parent template and specify placeholders. This can be achieved with a base_layout
function:
import datetime\n\nfrom htpy import body, div, h1, head, html, p, title, Node, Element\n\n\ndef base_page(*,\n page_title: str | None = None,\n extra_head: Node = None,\n content: Node = None,\n body_class: str | None = None,\n) -> Element:\n return html[\n head[title[page_title], extra_head],\n body(class_=body_class)[\n content,\n div(\"#footer\")[f\"Copyright {datetime.date.today().year} by Foo Inc.\"],\n ],\n ]\n\n\ndef index_page() -> Element:\n return base_page(\n page_title=\"Welcome!\",\n body_class=\"green\",\n content=[\n h1[\"Welcome to my site!\"],\n p[\"Hello and welcome!\"],\n ],\n )\n\n\ndef about_page() -> Element:\n return base_page(\n page_title=\"About us\",\n content=[\n h1[\"About us\"],\n p[\"We love creating web sites!\"],\n ],\n )\n
"},{"location":"common-patterns/#ui-components","title":"UI components","text":"Creating higher level wrappers for common UI components can be a good idea to reduce repetition.
Wrapping Bootstrap Modal could be achieved with a function like this:
Creating wrapper for Bootstrap Modalfrom markupsafe import Markup\n\nfrom htpy import Element, Node, button, div, h5, span\n\n\ndef bootstrap_modal(*, title: str, body: Node = None, footer: Node = None) -> Element:\n return div(\".modal\", tabindex=\"-1\", role=\"dialog\")[\n div(\".modal-dialog\", role=\"document\")[\n div(\".modal-content\")[\n div(\".modal-header\")[\n div(\".modal-title\")[\n h5(\".modal-title\")[title],\n button(\n \".close\",\n type=\"button\",\n data_dismiss=\"modal\",\n aria_label=\"Close\",\n )[span(aria_hidden=\"true\")[Markup(\"×\")]],\n ]\n ],\n div(\".modal-body\")[body],\n footer and div(\".modal-footer\")[footer],\n ]\n ]\n ]\n
You would then use it like this:
from htpy import button, p\n\nprint(\n bootstrap_modal(\n title=\"Modal title\",\n body=p[\"Modal body text goes here.\"],\n footer=[\n button(\".btn.btn-primary\", type=\"button\")[\"Save changes\"],\n button(\".btn.btn-secondary\", type=\"button\")[\"Close\"],\n ],\n )\n)\n
"},{"location":"django/","title":"Usage with Django","text":"htpy is not tied to any specific web framework. Nonetheless, htpy works great when combined with Django. This page contains information and useful techniques on how to combine htpy and Django.
"},{"location":"django/#returning-a-htpy-response","title":"Returning a htpy response","text":"htpy elements can be passed directly to HttpResponse
:
from django.http import HttpResponse\nfrom htpy import html, body, div\n\ndef my_view(request):\n return HttpResponse(html[body[div[\"Hi Django!\"]]])\n
"},{"location":"django/#using-htpy-as-part-of-an-existing-django-template","title":"Using htpy as part of an existing Django template","text":"htpy elements are marked as \"safe\" and can be injected directly into Django templates. This can be useful if you want to start using htpy gradually in an existing template based Django project:
base.html<html>\n <head>\n <title>My Django Site</title>\n </head>\n <body>{{ content }}</body>\n</html>\n
views.pyfrom django.shortcuts import render\n\nfrom htpy import h1\n\n\ndef index(request):\n return render(request, \"base.html\", {\n \"content\": h1[\"Welcome to my site!\"],\n })\n
"},{"location":"django/#render-a-django-form","title":"Render a Django form","text":"CSRF token, form widgets and errors can be directly used within htpy elements:
forms.pyfrom django import forms\n\n\nclass MyForm(forms.Form):\n name = forms.CharField()\n
views.pyfrom django.http import HttpRequest, HttpResponse\n\nfrom .components import my_form_page, my_form_success_page\nfrom .forms import MyForm\n\n\ndef my_form(request: HttpRequest) -> HttpResponse:\n form = MyForm(request.POST or None)\n if form.is_valid():\n return HttpResponse(my_form_success_page())\n\n return HttpResponse(my_form_page(request, my_form=form))\n
components.pyfrom django.http import HttpRequest\nfrom django.template.backends.utils import csrf_input\n\nfrom htpy import Element, Node, body, button, form, h1, head, html, title\n\nfrom .forms import MyForm\n\n\ndef base_page(page_title: str, content: Node) -> Element:\n return html[\n head[title[page_title]],\n body[content],\n ]\n\n\ndef my_form_page(request: HttpRequest, *, my_form: MyForm) -> Element:\n return base_page(\n \"My form\",\n form(method=\"post\")[\n csrf_input(request),\n my_form.errors,\n my_form[\"name\"],\n button[\"Submit!\"],\n ],\n )\n\n\ndef my_form_success_page() -> Element:\n return base_page(\n \"Success!\",\n h1[\"Success! The form was valid!\"],\n )\n
"},{"location":"django/#implement-custom-form-widgets-with-htpy","title":"Implement custom form widgets with htpy","text":"You can implement a custom form widget directly with htpy like this:
widgets.pyfrom django.forms import widgets\n\nfrom htpy import sl_input\n\n\nclass ShoelaceInput(widgets.Widget):\n \"\"\"\n A form widget using Shoelace's <sl-input> element.\n More info: https://shoelace.style/components/input\n \"\"\"\n\n def render(self, name, value, attrs=None, renderer=None):\n return str(sl_input(attrs, name=name, value=value))\n
"},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#how-does-htpy-performance-compare-to-django-or-jinja-templates","title":"How does htpy performance compare to Django or Jinja templates?","text":"The performance of HTML rendering is rarely the bottleneck in most web application. It is usually fast enough regardless of what method of constructing the HTML is being used.
Given that it has been fast enough, there has not been much effort in optimizing htpy. It should be possible to significantly increase the effectiveness and we are open to contributions with benchmarks and speed improvements.
That said, htpy is currently on par with Django templates when it comes to speed. Jinja2 is currently significantly faster than both Django templates and htpy. There is a small benchmark script in the repo that generates a table with 50 000 rows.
"},{"location":"faq/#can-htpy-generate-xmlxhtml","title":"Can htpy generate XML/XHTML?","text":"No. Generating XML/XHTML is out of scope for this project. Use a XML library if you are looking to generate XML.
htpy generates HTML, therefore \"void elements\" such as <br>
does not include a trailing /
.
If you are looking to generate generic XML, lxml.builder
could be a good alternative.
With a template language, putting HTML markup in separate files is enforced by design. Avoiding logic in the presentation layer is also mostly done by making the language very restrictive.
It takes a little bit of planning and effort, but it is possible to have a nicely separated presentation layer that is free from logic. See Common patterns for more details on how you can structure your project.
"},{"location":"faq/#what-kind-of-black-magic-makes-from-htpy-import-whatever_element-work","title":"What kind of black magic makesfrom htpy import whatever_element
work?","text":"htpy uses the module level __getattr__
. It was introduced in Python 3.7. It allows creating Element
instances for any elements that are imported.
htpy must be compatible with standard Python code formatters, editors and static type checkers. Unfortunately, it is not possible to support those workflows with a custom syntax without a massive effort to change those tools to support that syntax.
"},{"location":"references/","title":"References","text":""},{"location":"references/#projects-that-use-htpy","title":"Projects that use htpy","text":"htpy was heavily inspired by many other libraries that came before it.
[]
syntax for specifying children. Not actively maintained.[]
syntax for children. Not actively maintained.()
) for attributes and getitem ([]
) for children.E
allows creating XML/XHTML documents from Python.htpy was designed to be used with static typing. Since you define all your own data/components with regular Python, a static type checker like mypy will catch errors like this:
class User:\n def __init__(self, name: str):\n self.name = name\n\n\ndef greeting(user: User) -> Element:\n return h1[f\"Hi {user.first_name.capitalize()}!]\n# ^^^^^^^^^^\n# mypy: error: \"User\" has no attribute \"first_name\" [attr-defined]\n
"},{"location":"static-typing/#autocompletion-of-html-elements","title":"Autocompletion of HTML elements","text":"htpy ships with type annotations for all HTML elements. If your editor supports it, it will show you useful auto completions:
"},{"location":"static-typing/#element-and-voidelement-classes","title":"Element
and VoidElement
classes","text":"The base types/classes in htpy are Element
and VoidElement
. Element
are all regular HTML elements that can have children such as <div>
, <span>
and <table>
. VoidElement
are HTML void element which cannot have children such as <img>
, <input>
and <br>
.
Use Element
as the return type when you want to always return an element.
from typing import Literal\n\nfrom htpy import Element, span\n\n\ndef bootstrap_badge(\n text: str,\n style: Literal[\"primary\", \"success\", \"danger\"] = \"primary\",\n) -> Element:\n return span(f\".badge.text-bg-{style}\")[text]\n
"},{"location":"static-typing/#node","title":"Node","text":"Node
is a type alias for all possible objects that can be used as a child node. See the source for the exact definition that defines all kinds of nodes that can be children of an element. This is a wider type than Element
since child nodes can be str, markup, None, iterables or callables.
Use Node
when you want to create a wrapper function to be flexible with what you accept. This function will accept both a str or some other element to be passed as contents
:
from htpy import Element, Node, div\n\ndef bootstrap_alert(contents: Node) -> Element:\n return div(\".alert\", role=\"alert\")[contents]\n
"},{"location":"streaming/","title":"Streaming of contents","text":"Internally, htpy is built with generators. Most of the time, you would render the full page with str()
, but htpy can also incrementally generate pages which can then be streamed to the browser. If your page uses a database or other services to retrieve data, you can sending the first part of the page to the client while the page is being generated.
Note
Streaming requires a bit of discipline and care to get right. You need to ensure to avoid doing too much work up front and use lazy constructs such as generators and callables. Most of the time, rendering the page without streaming will be the easiest way to get going. Streaming can give you improved user experience from faster pages/rendering.
This video shows what it looks like in the browser to generate a HTML table with Django StreamingHttpResponse (source code):
This example simulates a (very) slow fetch of data and shows the power of streaming: The browser loads CSS and gradually shows the contents. By loading CSS files in the <head>
tag before dynamic content, the browser can start working on loading the CSS and styling the page while the server keeps generating the rest of the page.
Django's querysets are lazily evaluated. They will not execute a database query before their value is actually needed.
This example shows how this property of Django querysets can be used to create a page that streams objects:
from django.http import StreamingHttpResponse\nfrom htpy import ul, li\n\nfrom myapp.models import Article\n\ndef article_list(request):\n return StreamingHttpResponse(ul[\n (li[article.title] for article in Article.objects.all())\n ])\n
"},{"location":"streaming/#using-callables-to-delay-evalutation","title":"Using callables to delay evalutation","text":"Pass a callable that does not accept any arguements as child to delay the evaluation.
This example shows how the page starts rendering and outputs the <h1>
tag and then calls calculate_magic_number
.
import time\nfrom htpy import div, h1\n\ndef calculate_magic_number() -> str:\n time.sleep(1)\n print(\" (running the complex calculation...)\")\n return \"42\"\n\nelement = div[\n h1[\"Welcome to my page\"],\n \"The magic number is \",\n calculate_magic_number,\n]\n\nfor chunk in element:\n print(chunk)\n
Output:
<div>\n<h1>\nWelcome to my page\n</h1>\nThe magic number is\n42 # <-- Appears after 3 seconds\n</div>\n
You may use lambda
to create a function without arguments to make a an expression lazy:
from htpy import div, h1\n\n\ndef fib(n: int) -> int:\n if n == 0:\n return 0\n elif n == 1:\n return 1\n else:\n return fib(n - 1) + fib(n - 2)\n\n\nprint(\n div[\n h1[\"Fibonacci!\"],\n \"fib(20)=\",\n lambda: str(fib(20)),\n ]\n)\n# output: <div><h1>Fibonacci!</h1>fib(12)=6765</div>\n
"},{"location":"usage/","title":"Usage","text":"Elements are imported directly from the htpy
module as their name. HTML attributes are specified by parenthesis (()
/ \"call\"). Children are specified using square brackets ([]
/ \"getitem\").
>>> from htpy import div\n>>> print(div(id=\"hi\")[\"Hello!\"])\n<div id=\"hi\">Hello!</div>\n
"},{"location":"usage/#elements","title":"Elements","text":"Children can be strings, markup, other elements or lists/iterators.
Elements can be arbitrarily nested: Nested elements
>>> from htpy import article, section, p\n>>> print(section[article[p[\"Lorem ipsum\"]]])\n<section><article><p>Lorem ipsum</p></article></section>\n
"},{"location":"usage/#textstrings","title":"Text/strings","text":"It is possible to pass a string directly: Using a string as children
>>> from htpy import h1\n>>> print(h1[\"Welcome to my site!\"])\n<h1>Welcome to my site!</h1>\n
Strings are automatically escaped to avoid XSS vulnerabilities. It is convenient and safe to directly insert variable data via f-strings:
>>> from htpy import h1\n>>> user_supplied_name = \"bobby </h1>\"\n>>> print(h1[f\"hello {user_supplied_name}\"])\n<h1>hello bobby </h1></h1>\n
"},{"location":"usage/#conditional-rendering","title":"Conditional rendering","text":"None
will not render anything. This can be useful to conditionally render some content.
>>> from htpy import div, b\n>>> error = None\n\n>>> # No <b> tag will be rendered since error is None\n>>> print(div[error and b[error]])\n<div></div>\n\n>>> error = 'Enter a valid email address.'\n>>> print(div[error and b[error]])\n<div><b>Enter a valid email address.</b></div>\n\n# Inline if/else can also be used:\n>>> print(div[b[error] if error else None])\n<div><b>Enter a valid email address.</b></div>\n
"},{"location":"usage/#loops-iterating-over-children","title":"Loops / iterating over children","text":"You can pass a list, tuple or generator to generate multiple children:
Iterate over a generator>>> from htpy import ul, li\n>>> print(ul[(li[letter] for letter in \"abc\")])\n<ul><li>a</li><li>b</li><li>c</li></ul>\n
Note
The generator will be lazily evaluated when rendering the element, not directly when the element is constructed. See Streaming for more information.
A list
can be used similar to a JSX fragment:
>>> from htpy import div, img\n>>> my_images = [img(src=\"a.jpg\"), img(src=\"b.jpg\")]\n>>> print(div[my_images])\n<div><img src=\"a.jpg\"><img src=\"b.jpg\"></div>\n
"},{"location":"usage/#custom-elements-web-components","title":"Custom elements / web components","text":"Custom elements / web components are HTML elements that contains at least one dash (-
). Since -
cannot be used in Python identifiers, use underscore (_
) instead:
>>> from htpy import my_custom_element\n>>> print(my_custom_element['hi!'])\n<my-custom-element>hi!</my-custom-element>\n
"},{"location":"usage/#injecting-markup","title":"Injecting markup","text":"If you have HTML markup that you want to insert without further escaping, wrap it in Markup
from the markupsafe library. markupsafe is a dependency of htpy and is automatically installed:
>>> from htpy import div\n>>> from markupsafe import Markup\n>>> print(div[Markup(\"<foo></foo>\")])\n<div><foo></foo></div>\n
If you are generate Markdown and want to insert it into an element, use Markup
: Injecting generated markdown
>>> from markdown import markdown\n>>> from markupsafe import Markup\n>>> from htpy import div\n>>> print(div[Markup(markdown('# Hi'))])\n<div><h1>Hi</h1></div>\n
"},{"location":"usage/#html-doctype","title":"HTML Doctype","text":"The HTML doctype is automatically prepended to the <html>
tag:
>>> from htpy import html\n>>> print(html)\n<!doctype html><html></html>\n
"},{"location":"usage/#attributes","title":"Attributes","text":"HTML attributes are defined by calling the element. They can be specified in a couple of different ways.
"},{"location":"usage/#elements-without-attributes","title":"Elements without attributes","text":"Some elements do not have attributes, they can be specified by just the element itself:
>>> from htpy import hr\n>>> print(hr)\n<hr>\n
"},{"location":"usage/#keyword-arguments","title":"Keyword arguments","text":"Attributes can be specified via keyword arguments:
>>> from htpy import img\n>>> print(img(src=\"picture.jpg\"))\n<img src=\"picture.jpg\">\n
In Python, class
and for
cannot be used as keyword arguments. Instead, they can be specified as class_
or for_
when using keyword arguments:
>>> from htpy import label\n>>> print(label(for_=\"myfield\"))\n<label for=\"myfield\"></label>\n
Attributes that contains dashes -
can be specified using underscores:
>>> from htpy import form\n>>> print(form(hx_post=\"/foo\"))\n<form hx-post=\"/foo\"></form>\n
"},{"location":"usage/#idclass-shorthand","title":"id/class shorthand","text":"Defining id
and class
attributes is common when writing HTML. A string shorthand that looks like a CSS selector can be used to quickly define id and classes:
>>> from htpy import div\n>>> print(div(\"#myid\"))\n<div id=\"myid\"></div>\n
Define multiple classes>>> from htpy import div\n>>> print(div(\".foo.bar\"))\n<div id=\"foo bar\"></div>\n
Combining both id and classes>>> from htpy import div\n>>> print(div(\"#myid.foo.bar\"))\n<div id=\"myid\" class=\"foo bar\"></div>\n
"},{"location":"usage/#attributes-as-dict","title":"Attributes as dict","text":"Attributes can also be specified as a dict
. This is useful when using attributes that are reserved Python keywords (like for
or class
), when the attribute name contains a dash (-
) or when you want to define attributes dynamically.
>>> from htpy import div\n>>> print(div({\"data-foo\": \"bar\"}))\n<div data-foo=\"bar\"></div>\n
Using an attribute with a reserved keyword>>> from htpy import label\n>>> print(label({\"for\": \"myfield\"}))\n<label for=\"myfield\"></label>\n
"},{"location":"usage/#boolean-attributes","title":"Boolean attributes","text":"In HTML, boolean attributes such as disabled
are considered \"true\" when they exist. Specifying an attribute as True
will make it appear (without a value). False
will make it hidden. This is useful and brings the semantics of bool
to HTML.
>>> from htpy import button\n>>> print(button(disabled=True))\n<button disabled></button>\n
False bool attribute>>> from htpy import button\n>>> print(button(disabled=False))\n<button></button>\n
"},{"location":"usage/#conditionally-mixing-css-classes","title":"Conditionally mixing CSS classes","text":"To make it easier to mix CSS classes, the class
attribute accepts a list of class names or a dict. Falsey values will be ignored.
>>> from htpy import button\n>>> is_primary = True\n>>> print(button(class_=[\"btn\", {\"btn-primary\": is_primary}]))\n<button class=\"btn btn-primary\"></button>\n>>> is_primary = False\n>>> print(button(class_=[\"btn\", {\"btn-primary\": is_primary}]))\n<button class=\"btn\"></button>\n>>>\n
"},{"location":"usage/#combining-modes","title":"Combining modes","text":"Attributes via id/class shorthand, keyword arguments and dictionary can be combined:
Specifying attribute via multiple arguments>>> from htyp import label\n>>> print(label(\"#myid.foo.bar\", {'for': \"somefield\"}, name=\"myname\",))\n<label id=\"myid\" class=\"foo bar\" for=\"somefield\" name=\"myname\"></label>\n
"},{"location":"usage/#iterating-of-the-output","title":"Iterating of the output","text":"Iterating over a htpy element will yield the resulting contents in chunks as they are rendered:
>>> from htpy import ul, li\n>>> for chunk in ul[li[\"a\", \"b\"]]:\n... print(f\"got a chunk: {chunk!r}\")\n...\ngot a chunk: '<ul>'\ngot a chunk: '<li>'\ngot a chunk: 'a'\ngot a chunk: 'b'\ngot a chunk: '</li>'\ngot a chunk: '</ul>'\n
"}]}
\ No newline at end of file
diff --git a/sitemap.xml.gz b/sitemap.xml.gz
index 3a126bf864f25669e271137ac713f7204077d3cb..0a3cb06f8d5b11e4d038735987c442e409be0842 100644
GIT binary patch
delta 13
Ucmb=gXP58h;Lw`mJ(0Zv02;po;Q#;t
delta 13
Ucmb=gXP58h;NZ2