loguru icon indicating copy to clipboard operation
loguru copied to clipboard

Compatibility with IPython / prompt toolkit

Open GDYendell opened this issue 3 months ago • 2 comments

Console output from loguru seems to bypass the buffering IPython does to keep its user prompt pinned to the bottom, so you get this:

In [1]: 2025-09-23 16:29:39.811 | INFO     | __main__:background_logger:13 - Log message 1
2025-09-23 16:29:40.812 | INFO     | __main__:background_logger:13 - Log message 2
2025-09-23 16:29:41.812 | INFO     | __main__:background_logger:13 - Log message 3
In [2]:

This does not happen with ~~std logging~~ structlog using the defaults, which uses print() to render to console. The behaviour is the same std logging.

Reproduce with:

import threading
import time

import IPython
from loguru import logger


def background_logger():
    count = 0
    while count < 3:
        time.sleep(1)
        count += 1
        logger.info(f"Log message {count}")


threading.Thread(target=background_logger, daemon=True).start()

IPython.embed()

Is there any way to make this work nicely?

GDYendell avatar Sep 23 '25 16:09 GDYendell

This seems to work

import threading
import time

import IPython
from loguru import logger
from prompt_toolkit.patch_stdout import StdoutProxy

logger.remove()
logger.add(StdoutProxy(raw=True))


def background_logger():
    count = 0
    while count < 3:
        time.sleep(1)
        count += 1
        logger.info(f"Log message {count}")


threading.Thread(target=background_logger, daemon=True).start()

IPython.embed()
2025-09-25 10:38:54.884 | INFO     | __main__:background_logger:20 - Log message 1
2025-09-25 10:38:55.885 | INFO     | __main__:background_logger:20 - Log message 2
2025-09-25 10:38:56.885 | INFO     | __main__:background_logger:20 - Log message 3
In [1]:

Although there are typing errors

No overloads for "add" match the provided arguments

because


class Writable(Protocol):
    def write(self, message: Message) -> None: ...

doesn't match

class StdoutProxy():
    ...
    
    def write(self, data: str) -> int:
        ...

I wonder if this could be allowed in the Writable protocol?

GDYendell avatar Sep 25 '25 10:09 GDYendell

Hi. Thanks for the reproducible example.

What happens is that IPython replaces the default sys.stderr (_io.TextIOWrapper) with their own custom stream object (prompt_toolkit.patch_stdout.StdoutProxy) while the interactive terminal started by IPython.embed() is active. Additionally, when Loguru is imported, it setups its default sink based on the current sys.stderr value. Therefore, while IPython later replaces sys.stderr, Loguru will still write to the original stream instead of the new StdoutProxy. Consequently, logs do not go through the proxy which would allow them to be properly aligned above the prompt.

Given the inherent instability of sys.stderr in such case, the most elegant solution is to use a basic wrapper function that dynamically retrieves the current error stream. This ensures logging behaves consistently, similar to print(), regardless of whether execution occurs during an interactive IPython session.

import threading
import time

import IPython
from loguru import logger
import sys

# Force the sys.stderr value to be dynamically fetched since IPython replaces it.
logger.remove()
logger.add(lambda msg: sys.stderr.write(msg), colorize=True)


def background_logger():
    count = 0
    while count < 3:
        time.sleep(1)
        count += 1
        logger.info(f"Log message {count}")


threading.Thread(target=background_logger, daemon=True).start()

IPython.embed()

Regarding the compatibility of StdoutProxy as a sink:

I wonder if this could be allowed in the Writable protocol?

Yes, this is a very good point. The type hint defined by Loguru was inaccurate, requiring the write() method to return None while it is in fact irrelevant (and failing for StdoutProxy since it returns an int). I updated the Writable protocol and such stream-like object should be accepted now.

Delgan avatar Sep 28 '25 13:09 Delgan