Add support for t-strings in the stdlib logging library

If logging supports t-strings, this would work with a lazy string wrapper:

class Lazy:
    def __init__(self, callback):
        self.callback = callback
    def __str__(self):
        return str(self.callback())

logger.debug(t'Message with {Lazy(expensive_func1)}, {Lazy(expensive_func2)}')
2 Likes

I guess this is the best possible workaround given the suboptimal circumstances.

True, but they do accept function names, which can be evaluated later. Example from tstring-util · PyPI

from tstring import render
def hello(name):
    print(f"hello {name}")

def test_lazy():
    who = 'bob'
    flavor = 'spicy'
    embedx = t'Call function {hello:!fn} {who} {flavor}'
    who = 'jane'
    r = render(embedx)
    assert r ==  "Call function hello jane spicy"

The key concept is a “t-string” is not so much a string as a complex object designed for an input into a library to do whatever it wants with it.

The stdlib logging library could define format specifiers to do interesting things.

Beyond the “!fn” I used for tellling render to evaluate the proceeding function name, logging could do clever things like conditionally format based on logging level.

basic = "hello"
complex = "yada yada"
msg = t"Message with  {basic:info} {"and:debug"}  {complex:debug}
logger.info(msg) -> Message with hello
loggin\g.deb(msg) ->  Message with hello and yada yada

I’m not saying this is necessarily a good idea, merely that there are a lot of opportunities due to the open-ended nature of the format specifier.

Well the workaround only works if t-strings are supported in the first place so we should still be supportive of the pursuit of this idea unless something better comes around.

Interesting, but, not sure if i following your code, How do you pass the argument to the “hello” function?

It’s baked into the render implementation. The !fn tells render to treat the preceding symbol as a function, inspect the function to see how many parameters it needs, and then consume the next N interpolations.

In hindsight, the example was poor, so I’ve updated it to:

from tstring import render

def double(value):
    print(f"twice {value} is {2*value}")

def test_lazy():
    number = 1
    flavor = 'spicy'
    embedx = t'Call function {double:!fn} {number} {flavor}'
    number = 2


    r = render(embedx)
    assert r ==  "Call function twice 2 is 4 spicy"
    print(r)

In response to the recommendations here, a better implementation of lazy logging wrapper class would be:

class LazyLog:
    def __init__(self, func: Callable[[], object]) -> None:
        self.func = func
    def __format__(self, fmt: str) -> str:
        return self.func().__format__(fmt)

Quite a bit off topic, but why did you choose to capture stdout from the function rather than using the return value?