Skip to content

Commit 785e489

Browse files
feat: add outstream hook similar to display publisher
Similar to ipython#115 but for stdout/stderr This can be useful for a thread to redirect stdout/stderr to a file, while other threads can still write to stdout/stderr.
1 parent 4f72a6c commit 785e489

File tree

1 file changed

+60
-3
lines changed

1 file changed

+60
-3
lines changed

ipykernel/iostream.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from io import StringIO, TextIOBase
1616
from typing import Any, Callable, Deque, Optional
1717
from weakref import WeakSet
18+
from threading import local
1819

1920
import zmq
2021
from jupyter_client.session import extract_header
@@ -403,6 +404,7 @@ def __init__(
403404
self.echo = None
404405
self._isatty = bool(isatty)
405406
self._should_watch = False
407+
self._local = local()
406408

407409
if (
408410
watchfd
@@ -525,11 +527,19 @@ def _flush(self):
525527
# There should be a better way to do this.
526528
self.session.pid = os.getpid()
527529
content = {"name": self.name, "text": data}
530+
msg = self.session.msg("stream", content, parent=self.parent_header)
531+
532+
# Each transform either returns a new
533+
# message or None. If None is returned,
534+
# the message has been 'used' and we return.
535+
for hook in self._hooks:
536+
msg = hook(msg)
537+
if msg is None:
538+
return
539+
528540
self.session.send(
529541
self.pub_thread,
530-
"stream",
531-
content=content,
532-
parent=self.parent_header,
542+
msg,
533543
ident=self.topic,
534544
)
535545

@@ -601,3 +611,50 @@ def _rotate_buffer(self):
601611
old_buffer = self._buffer
602612
self._buffer = StringIO()
603613
return old_buffer
614+
615+
@property
616+
def _hooks(self):
617+
if not hasattr(self._local, "hooks"):
618+
# create new list for a new thread
619+
self._local.hooks = []
620+
return self._local.hooks
621+
622+
623+
def register_hook(self, hook):
624+
"""
625+
Registers a hook with the thread-local storage.
626+
627+
Parameters
628+
----------
629+
hook : Any callable object
630+
631+
Returns
632+
-------
633+
Either a publishable message, or `None`.
634+
The hook callable must return a message from
635+
the __call__ method if they still require the
636+
`session.send` method to be called after transformation.
637+
Returning `None` will halt that execution path, and
638+
session.send will not be called.
639+
"""
640+
self._hooks.append(hook)
641+
642+
def unregister_hook(self, hook):
643+
"""
644+
Un-registers a hook with the thread-local storage.
645+
646+
Parameters
647+
----------
648+
hook : Any callable object which has previously been
649+
registered as a hook.
650+
651+
Returns
652+
-------
653+
bool - `True` if the hook was removed, `False` if it wasn't
654+
found.
655+
"""
656+
try:
657+
self._hooks.remove(hook)
658+
return True
659+
except ValueError:
660+
return False

0 commit comments

Comments
 (0)