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?

I don’t recall right now. Wasn’t the best decision and new version (tstring-util · PyPI) has an embed function that just uses the return value.

I’ve put template-string-loggerinto PyPI.

tstring-logger

Python logging with template strings with support for conditional (lazy) execution of functions in log messages.

Provides TStringLogger(logging.Logger) and sets it as the default logger class upon import tstring_logger

A Template String may embed function calls with a !fn format specifier. They will only be executed if the logger is active.

Example

import logging
import tstring_logger

def expensive_function(x):
    time.sleep(1)
    return 2 * x

logging.basicConfig()
lg = logging.getLogger("demo")
hour = datetime.datetime.now().hour
print(1)
lg.debug(test := t"The thing happened with {expensive_function:!fn} {7} at hour {hour}.")
print(2)
lg.setLevel(logging.DEBUG)
lg.debug(test)
print(3)

will quickly print 1 and 2, then pause a second before 3 appears after DEBUG:demo:The thing happened with 14 at hour 16.
Since expensive_function takes a single argument, the 7 is passed to the function while hour renders in the usual way.

The logger uses the embed function of tstring-util.

Late and optional string conversion and concatenation is EXACTLY what the usage of % templates achieve when logging. It would be just equivalent, but much more convenient to use

But { - str.format like templates would still be less convenient than f-strings. t-strings are on convenience parity: no typing names twice, no positional argument counting