Skip to content

Commit 180dd4f

Browse files
authored
properly close OutStream and various fixes (#1305)
1 parent 3d4df3b commit 180dd4f

File tree

8 files changed

+74
-25
lines changed

8 files changed

+74
-25
lines changed

ipykernel/_version.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
store the current version info of the server.
33
"""
4+
from __future__ import annotations
5+
46
import re
57

68
# Version string must appear intact for hatch versioning

ipykernel/embed.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@ def embed_kernel(module=None, local_ns=None, **kwargs):
5555
app.kernel.user_ns = local_ns
5656
app.shell.set_completer_frame() # type:ignore[union-attr]
5757
app.start()
58+
app.close()

ipykernel/inprocess/channels.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# Copyright (c) IPython Development Team.
44
# Distributed under the terms of the Modified BSD License.
5+
from __future__ import annotations
56

67
from jupyter_client.channelsabc import HBChannelABC
78

ipykernel/iostream.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,8 @@ def fileno(self):
398398
"""
399399
Things like subprocess will peak and write to the fileno() of stderr/stdout.
400400
"""
401-
if getattr(self, "_original_stdstream_copy", None) is not None:
402-
return self._original_stdstream_copy
401+
if getattr(self, "_original_stdstream_fd", None) is not None:
402+
return self._original_stdstream_fd
403403
msg = "fileno"
404404
raise io.UnsupportedOperation(msg)
405405

@@ -529,10 +529,7 @@ def __init__(
529529
# echo on the _copy_ we made during
530530
# this is the actual terminal FD now
531531
echo = io.TextIOWrapper(
532-
io.FileIO(
533-
self._original_stdstream_copy,
534-
"w",
535-
)
532+
io.FileIO(self._original_stdstream_copy, "w", closefd=False)
536533
)
537534
self.echo = echo
538535
else:
@@ -597,9 +594,10 @@ def close(self):
597594
self._should_watch = False
598595
# thread won't wake unless there's something to read
599596
# writing something after _should_watch will not be echoed
600-
os.write(self._original_stdstream_fd, b"\0")
601-
if self.watch_fd_thread is not None:
597+
if self.watch_fd_thread is not None and self.watch_fd_thread.is_alive():
598+
os.write(self._original_stdstream_fd, b"\0")
602599
self.watch_fd_thread.join()
600+
self.echo = None
603601
# restore original FDs
604602
os.dup2(self._original_stdstream_copy, self._original_stdstream_fd)
605603
os.close(self._original_stdstream_copy)

ipykernel/kernelapp.py

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ class IPKernelApp(BaseIPythonApplication, InteractiveShellApp, ConnectionFileMix
151151

152152
_ports = Dict()
153153

154+
_original_io = Any()
155+
_log_map = Any()
156+
_io_modified = Bool(False)
157+
_blackhole = Any()
158+
154159
subcommands = {
155160
"install": (
156161
"ipykernel.kernelspec.InstallIPythonKernelSpecApp",
@@ -470,56 +475,93 @@ def log_connection_info(self):
470475

471476
def init_blackhole(self):
472477
"""redirects stdout/stderr to devnull if necessary"""
478+
self._save_io()
473479
if self.no_stdout or self.no_stderr:
474-
blackhole = open(os.devnull, "w") # noqa: SIM115
480+
# keep reference around so that it would not accidentally close the pipe fds
481+
self._blackhole = open(os.devnull, "w") # noqa: SIM115
475482
if self.no_stdout:
476-
sys.stdout = sys.__stdout__ = blackhole # type:ignore[misc]
483+
if sys.stdout is not None:
484+
sys.stdout.flush()
485+
sys.stdout = self._blackhole
477486
if self.no_stderr:
478-
sys.stderr = sys.__stderr__ = blackhole # type:ignore[misc]
487+
if sys.stderr is not None:
488+
sys.stderr.flush()
489+
sys.stderr = self._blackhole
479490

480491
def init_io(self):
481492
"""Redirect input streams and set a display hook."""
493+
self._save_io()
482494
if self.outstream_class:
483495
outstream_factory = import_item(str(self.outstream_class))
484-
if sys.stdout is not None:
485-
sys.stdout.flush()
486496

487-
e_stdout = None if self.quiet else sys.__stdout__
488-
e_stderr = None if self.quiet else sys.__stderr__
497+
e_stdout = None if self.quiet else sys.stdout
498+
e_stderr = None if self.quiet else sys.stderr
489499

490500
if not self.capture_fd_output:
491501
outstream_factory = partial(outstream_factory, watchfd=False)
492502

503+
if sys.stdout is not None:
504+
sys.stdout.flush()
493505
sys.stdout = outstream_factory(self.session, self.iopub_thread, "stdout", echo=e_stdout)
506+
494507
if sys.stderr is not None:
495508
sys.stderr.flush()
496509
sys.stderr = outstream_factory(self.session, self.iopub_thread, "stderr", echo=e_stderr)
510+
497511
if hasattr(sys.stderr, "_original_stdstream_copy"):
498512
for handler in self.log.handlers:
499-
if isinstance(handler, StreamHandler) and (handler.stream.buffer.fileno() == 2):
513+
if (
514+
isinstance(handler, StreamHandler)
515+
and (buffer := getattr(handler.stream, "buffer", None))
516+
and (fileno := getattr(buffer, "fileno", None))
517+
and fileno() == sys.stderr._original_stdstream_fd # type:ignore[attr-defined]
518+
):
500519
self.log.debug("Seeing logger to stderr, rerouting to raw filedescriptor.")
501-
502-
handler.stream = TextIOWrapper(
503-
FileIO(
504-
sys.stderr._original_stdstream_copy,
505-
"w",
506-
)
520+
io_wrapper = TextIOWrapper(
521+
FileIO(sys.stderr._original_stdstream_copy, "w", closefd=False)
507522
)
523+
self._log_map[id(io_wrapper)] = handler.stream
524+
handler.stream = io_wrapper
508525
if self.displayhook_class:
509526
displayhook_factory = import_item(str(self.displayhook_class))
510527
self.displayhook = displayhook_factory(self.session, self.iopub_socket)
511528
sys.displayhook = self.displayhook
512529

513530
self.patch_io()
514531

532+
def _save_io(self):
533+
if not self._io_modified:
534+
self._original_io = sys.stdout, sys.stderr, sys.displayhook
535+
self._log_map = {}
536+
self._io_modified = True
537+
515538
def reset_io(self):
516539
"""restore original io
517540
518541
restores state after init_io
519542
"""
520-
sys.stdout = sys.__stdout__
521-
sys.stderr = sys.__stderr__
522-
sys.displayhook = sys.__displayhook__
543+
if not self._io_modified:
544+
return
545+
stdout, stderr, displayhook = sys.stdout, sys.stderr, sys.displayhook
546+
sys.stdout, sys.stderr, sys.displayhook = self._original_io
547+
self._original_io = None
548+
self._io_modified = False
549+
if finish_displayhook := getattr(displayhook, "finish_displayhook", None):
550+
finish_displayhook()
551+
if hasattr(stderr, "_original_stdstream_copy"):
552+
for handler in self.log.handlers:
553+
if orig_stream := self._log_map.get(id(handler.stream)):
554+
self.log.debug("Seeing modified logger, rerouting back to stderr")
555+
handler.stream = orig_stream
556+
self._log_map = None
557+
if self.outstream_class:
558+
outstream_factory = import_item(str(self.outstream_class))
559+
if isinstance(stderr, outstream_factory):
560+
stderr.close()
561+
if isinstance(stdout, outstream_factory):
562+
stdout.close()
563+
if self._blackhole:
564+
self._blackhole.close()
523565

524566
def patch_io(self):
525567
"""Patch important libraries that can't handle sys.stdout forwarding"""

ipykernel/pickleutil.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
# Copyright (c) IPython Development Team.
44
# Distributed under the terms of the Modified BSD License.
5+
from __future__ import annotations
6+
57
import copy
68
import pickle
79
import sys

ipykernel/thread.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""Base class for threads."""
2+
from __future__ import annotations
3+
24
import typing as t
35
from threading import Event, Thread
46

tests/test_kernelapp.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def test_blackhole():
3131
app.no_stderr = True
3232
app.no_stdout = True
3333
app.init_blackhole()
34+
app.close()
3435

3536

3637
def test_start_app():

0 commit comments

Comments
 (0)