Monday 25 October 2021

hook into the builtin python f-string format machinery

Summary

I really LOVE f-strings. They're bloody awesome syntax.

For a while now I've had an idea for a little library- described below- to harness them further. A quick example of what I would like it do:

>>> import simpleformatter as sf
>>> def format_camel_case(string):
...     """camel cases a sentence"""
...     return ''.join(s.capitalize() for s in string.split())
...
>>> @sf.formattable(camcase=format_camel_case)
... class MyStr(str): ...
...
>>> f'{MyStr("lime cordial delicious"):camcase}'
'LimeCordialDelicious'

It would be immensely useful-- for the purposes of a simplified API, and extending usage to built-in class instances-- to find a way to hook into the builtin python formatting machinery, which would allow the custom format specification of built-ins:

>>> f'{"lime cordial delicious":camcase}'
'LimeCordialDelicious'

In other words, I'd like to override the built in format function (which is used by the f-string syntax)-- or alternatively, extend the built-in __format__ methods of existing standard library classes-- such that I could write stuff like this:

for x, y, z in complicated_generator:
    eat_string(f"x: {x:custom_spec1}, y: {x:custom_spec2}, z: {x:custom_spec3}")

I have accomplished this by creating subclasses with their own __format__ methods, but of course this will not work for built-in classes.

I could get close to it using the string.Formatter api:

my_formatter=MyFormatter()  # custom string.Formatter instance

format_str = "x: {x:custom_spec1}, y: {x:custom_spec2}, z: {x:custom_spec3}"

for x, y, z in complicated_generator:
    eat_string(my_formatter.format(format_str, **locals()))

I find this to be a tad clunky, and definitely not readable compared to the f-string api.

Another thing that could be done is overriding builtins.format:

>>> import builtins
>>> builtins.format = lambda *args, **kwargs: 'womp womp'
>>> format(1,"foo")
'womp womp'

...but this doesn't work for f-strings:

>>> f"{1:foo}"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Invalid format specifier

Details

Currently my API looks something like this (somewhat simplified):

import simpleformatter as sf
@sf.formatter("this_specification")
def this_formatting_function(some_obj):
    return "this formatted someobj!"

@sf.formatter("that_specification")
def that_formatting_function(some_obj):
    return "that formatted someobj!"

@sf.formattable
class SomeClass: ...

After which you can write code like this:

some_obj = SomeClass()
f"{some_obj:this_specification}"
f"{some_obj:that_specification}"

I would like the api to be more like the below:

@sf.formatter("this_specification")
def this_formatting_function(some_obj):
    return "this formatted someobj!"

@sf.formatter("that_specification")
def that_formatting_function(some_obj):
    return "that formatted someobj!"

class SomeClass: ...  # no class decorator needed

...and allow use of custom format specs on built-in classes:

x=1  # built-in type instance
f"{x:this_specification}"
f"{x:that_specification}"

But in order to do these things, we have to burrow our way into the built-in format() function. How can I hook into that juicy f-string goodness?



from hook into the builtin python f-string format machinery

No comments:

Post a Comment