I would like to pimp format_html()
of Django.
It already works quite nicely, but my IDE (PyCharm) thinks the variables are not used and paints them in light-gray color:
AFAIK f-strings use some magic rewriting.
Is there a way to implement this, so that the IDE knows that the variables get used?
Related: Implement f-string like syntax, with Django SafeString support
Here is my current implementation:
def h(html):"""Django's format_html() on steroids"""def replacer(match):call_frame = sys._getframe(3)return conditional_escape(eval(match.group(1), call_frame.f_globals, call_frame.f_locals))return mark_safe(re.sub(r'{(.*?)}', replacer, html))
Somebody raised security concerns: I don't plan to create CMS where a user can edit these templates. These template h-strings are only for developers to have a convenient way to create HTML.
Before writing an answer, be sure you know the magic of conditional_escape()
Since you don’t seem above using dirty hacks, here’s a hack even dirtier than the one in the question:
class _escaper(dict):def __init__(self, other):super().__init__(other)def __getitem__(self, name):return conditional_escape(super().__getitem__(name))_C = lambda value: (lambda: value).__closure__[0]
_F = type(_C)
try:type(_C(None))(None)
except:pass
else:_C = type(_C(None))def h(f):if not isinstance(f, _F):raise TypeError(f"expected a closure, a {type(f).__name__} was given")closure = Noneif f.__closure__:closure = tuple(_C(conditional_escape(cell.cell_contents))for cell in f.__closure__)fp = _F(f.__code__, _escaper(f.__globals__),f.__name__, f.__defaults__, closure)return mark_safe(fp())
The h
function takes a closure, and for each variable closed over, it creates another, escaped copy of the variable, and modifies the closure to capture that copy instead. The globals dict is also wrapped to ensure references to globals are likewise escaped. The modified closure is then immediately executed, its return value is marked safe and returned. So you must pass h
an ordinary function (no bound methods, for example) which accepts no arguments, preferably a lambda returning an f-string.
Your example then becomes:
foo = '&'
bar = h(lambda: f'<span>{foo}</span>')
assert h(lambda: f'<div>{bar}</div>') == '<div><span>&</span></div>'
There’s one thing, though. Due to how this has been implemented, you can only ever refer directly to variables inside interpolation points; no attribute accesses, no item lookups, nothing else. (You shouldn’t put string literals at interpolation points either.) Otherwise though, it works in CPython 3.9.2 and even in PyPy 7.3.3. I make no claims about it ever working in any other environment, or being in any way future-proof.