Very cool! After trying different approaches to render HTML from Python objects (including lxml, xml, etc.) I ended up liking htpy[0] the most, and the apps I built look similar to the examples in the FastHTML docs. I'll definitely try it.
One pattern I use is putting all the functions that generate HTML inside their own class. That way, I can more easily create and reuse components like:
class Views:
...
def comp1(self):
return Div(self.header(), P("too"))
Then `self.header()` can be reused in other parts, or to return partial HTML. It also makes it easy to pass the "request" object to the class, and do conditional rendering based on it (cookies, auth, language, etc).
Yes htpy is nice! Other interesting examples of functional HTML include Elm-html (Elm), hiccl (Common Lisp), hiccup (Clojure), Falco.Markup (F#), Lucid (Haskell), and dream-html (OCaml). FastHTML's system, called "FastTag" (FT) is a bit of a mashup of all of them plus some extra bits. I seriously considered just using htpy actually -- but in the end decided I preferred something a little different.
I've wondered about a class-based approach like that -- interesting to hear it's worked for you. I should try it! I'm using a purely functional approach for re-use, as you see in this example of the code for about.fastht.ml:
For what it's worth. One thing I really like about `htpy` is that the element attributes go before the child elements. I find this easier to write and read. Other things I like:
Having child elements as a list (i.e: the __getitem__ override) makes it convenient to build elements based on simple conditions + list comprehensions. This can be done with other frameworks, but it seems more natural to me when using `htpy`.
I also like that you can just `print()` elements and get the final HTML without having to pass it through a different function. This is not something specific about FastHTML, but rather something I've found I also had to do when using `lxml` or similar tools (I wrote about my experiments here[0])
I wrote a few things with each of FT and htpy, and looked at the resulting code -- I felt like the htpy approach was slightly less neat personally. htpy has the benefit that '.' and '#' can have special meanings, but the downside of needing to use both __getitem__ and __call__. I didn't feel like that was a tradeoff I wanted to make. I actually originally wrote FT for a different purpose (XML for language model input) so id and class attributes weren't of any interest at all at that time!
Also, I was able to implement FT using just 2 lines of code -- it felt like a very natural data structure that was a good fit with Python.
Having said all that, I think htpy is really nifty and elegant. :D
FastHTML is very interesting and reading this thread has led me to discover htpy as well which I am shocked I have never seen before! The htpy website and docs are also great. So now I am a bit of a dilemma over which one to use.
I actually hate working in HTML with all those closing tags etc so I nearly always set up a build/make process to edit my templates in PUG format. When I paste my PUG->html output into https://h2x.answer.ai/, or run html2htpy over them, I get python code that basically looks the same as those PUG templates. What a realization that is! So I may as well create and edit them in python rather than PUG and exploit the power of my beloved python dev environment and tools (as nicely stated in that "Throw out your templates" essay https://github.com/tavisrudd/throw_out_your_templates reference from the htpy docs). Thanks very much Jeremy and Andreas for this fantastic insight :)
I think these kinds of tools are awesome, although I normally prefer not having to rely on getting the functions/classes/types defined for me, I usually find it easier to just use lxml[0] to build HTML components from composable Python functions[1], similar to React.
I also built something similar for Python [0]. I decided to go with a separate DB file for the queue, I think it's better for performance but also to avoid mixing the data storage with the message queue (in my case, I assume the message queue is just an implementation detail and not really tied to the rest of the DB schema).
The Emacs version of this is using ^L (the ASCII form feed character), which lets you jump around using Emacs's built in page navigation commands.
I've used this occasionally in Emacs Lisp code and it's pretty nice, but I've never done it in other languages because I suspect that non-Emacs-users won't be able to handle it :P
A bit unrelated, but one thing I absolutely love is the fact you can install it by copying a single file to a folder in your PATH. I have been trying to follow this approach for my Python scripts (standard library only, everything in one file) and I really enjoy the experience. Most of the features I need only require Python 3.8, and Ubuntu comes with Python pre-installed, so
Yes, I've used shiv and PEX in the past, and I love those. But that means "adding a build step". Also, as far as I understand, by default, those tools normally decompress the generated zip file on startup, but the decompressed artefacts have to be manually cleaned.
I think they are worth for more complex apps (I actually have a draft blog post with some experiments I did using PEX + gunicorn), but for my use case, it's not worth the effort when I only need the standard library.
#! /usr/bin/env -S pip-run Jinja2==3.*
from jinja2 import Environment
env = Environment(autoescape=True)
template = env.from_string("Hello, {{ name }}!")
print(template.render(name="world"))
There is a downside. Because pip-run recreates the virtualenv every time, the script takes a second to start up. pipx will cache virtualenvs once the single-file script feature is released. I haven't used fades yet.
Edit: fades caches virtualenvs.
#! /usr/bin/env fades
from jinja2 import Environment # fades Jinja2==3.*
env = Environment(autoescape=True)
template = env.from_string("Hello, {{ name }}!")
print(template.render(name="world"))
I like to set up at least some tests on my scripts so that I can reduce the number of times I push something out that is obviously broken. pre-commit can also help with preventing shipping things with syntax errors if you enable the "ast" check, which does a simple syntax check on the code.
Some time ago, I wrote a queue using SQLite[0]. Instead of SKIP LOCKED, you can use RETURNING to lock-and-read a message and ensure only one worker is going to pick it up:
UPDATE ... SET status = 'locked' ... RETURNING message_id
Or you can just use an IMMEDIATE transaction, SELECT the next message ID to retrieve, and UPDATE the row.
On top of that, if you want to be extra safe, you can do:
UPDATE Queue SET status = 'locked' WHERE status = 'ready' AND message_id = '....'
To make sure you that the message you are trying to retrieve hasn't been locked already by another worker.
If I create a new login on mobile it never syncs to the vault/desktop correctly. I have so many empty vault items that then sync back to mobile. Nothing seems to fix it.
One pattern I use is putting all the functions that generate HTML inside their own class. That way, I can more easily create and reuse components like:
Then `self.header()` can be reused in other parts, or to return partial HTML. It also makes it easy to pass the "request" object to the class, and do conditional rendering based on it (cookies, auth, language, etc).[0]: https://htpy.dev/