Skip to content

Commit ebea865

Browse files
timsaucerclaude
andcommitted
docs: quiet the kernel noise in the docs build
`nb_execution_mode = "force"` starts a Jupyter kernel per executed page, and each start printed two messages that have nothing to do with this build: [IPKernelApp] WARNING | Kernel is running over TCP without encryption... UserWarning: IPython parent '<home>' is not a writable location... Together they accounted for every line of the build's stderr, which makes a real Sphinx warning easy to miss. The build log is now empty on success. The encryption notice is not describing a risk here. The kernel is a short-lived child process on the same machine, executing pages from this repository, reachable only over loopback. Neither remedy the message suggests works. `transport="ipc"` silences it and executes fine in isolation, but the real build then fails with "Kernel didn't respond in 60 seconds". `KernelManager.transport_encryption = "auto"` fails at kernel start on jupyter_client 8.9.1, which hands the provisioned CurveZMQ key to the client as `str` where a `bytes` trait is expected. So the message is filtered instead, by a logging filter matched to that one string rather than by `log_level = "ERROR"`, so any other kernel warning still reaches the log. If ipykernel rewords the message the filter stops matching and the warning returns, which is the right way for it to fail. The filter lives in `docs/ipython_kernel_config.py`, which `build.sh` copies into a throwaway `IPYTHONDIR` profile. It has to be an IPython profile rather than a `JUPYTER_CONFIG_PATH` entry, because `IPKernelApp` derives from `BaseIPythonApplication` and reads the IPython profile directory — `IPKernelApp().config_file_paths` is empty. Pointing `IPYTHONDIR` at a writable directory is also what removes the second message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b6c6f5b commit ebea865

4 files changed

Lines changed: 82 additions & 0 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ target
33
.idea
44
/docs/temp
55
/docs/build
6+
/docs/ipython
67
.DS_Store
78
.vscode
89

‎docs/build.sh‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,26 @@ fi
3434

3535
rm -rf build 2> /dev/null
3636
rm -rf temp 2> /dev/null
37+
rm -rf ipython 2> /dev/null
3738
mkdir temp
3839
cp -rf source/* temp/
3940

41+
# Give the notebook kernels a writable IPython profile of our own, and put
42+
# `ipython_kernel_config.py` in it. Two reasons, both about build output:
43+
#
44+
# * Without a writable IPYTHONDIR, IPython warns "IPython parent '<home>' is
45+
# not a writable location, using a temp directory" once per kernel start.
46+
# * The config file filters ipykernel's unencrypted-TCP notice, which
47+
# otherwise repeats once per executed page. See the comments in it.
48+
#
49+
# `IPKernelApp` reads the IPython profile directory rather than the Jupyter
50+
# config path, so this has to be an IPYTHONDIR profile and not a
51+
# `JUPYTER_CONFIG_PATH` entry. Kept out of temp/, which is the Sphinx source
52+
# directory for this build.
53+
export IPYTHONDIR="$script_dir/ipython"
54+
mkdir -p "$IPYTHONDIR/profile_default"
55+
cp ipython_kernel_config.py "$IPYTHONDIR/profile_default/"
56+
4057
# myst-nb executes each page as a notebook from the directory that page
4158
# lives in, so the example data files must sit alongside every page that
4259
# loads them by relative name (e.g. `ctx.read_csv("pokemon.csv")`). Symlink

‎docs/ipython_kernel_config.py‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
"""Config for the Jupyter kernel that executes the docs pages.
19+
20+
``build.sh`` copies this into a throwaway ``IPYTHONDIR`` profile, which is
21+
where ``IPKernelApp`` looks for it. It is not on the Jupyter config path:
22+
``IPKernelApp`` derives from ``BaseIPythonApplication`` and reads the IPython
23+
profile directory, so ``JUPYTER_CONFIG_PATH`` has no effect on it.
24+
"""
25+
26+
import logging
27+
28+
# ipykernel 7 warns at every kernel start that the ZeroMQ channels are
29+
# unencrypted TCP. With `nb_execution_mode = "force"` the docs build starts a
30+
# kernel per executed page, so the message repeats a dozen times per build and
31+
# buries the output that matters.
32+
#
33+
# It does not describe a risk this build has. The kernel is a short-lived child
34+
# process on the same machine, executing pages from this repository, reachable
35+
# only over loopback. There is no second party to the conversation.
36+
#
37+
# Neither remedy the message itself suggests is usable here:
38+
#
39+
# - `transport="ipc"` does silence it, and it did work in isolation, but the
40+
# real build then fails with "Kernel didn't respond in 60 seconds".
41+
# - `KernelManager.transport_encryption = "auto"` fails at kernel start on
42+
# jupyter_client 8.9.1, which hands the provisioned CurveZMQ key to the
43+
# client as `str` where a `bytes` trait is expected.
44+
#
45+
# So the message is filtered rather than the condition removed. A filter is
46+
# used in preference to `c.Application.log_level = "ERROR"` so that any *other*
47+
# kernel warning still reaches the build log. If ipykernel rewords the message
48+
# this filter stops matching and the warning comes back, which is the right way
49+
# for it to fail.
50+
_SUPPRESSED = "Kernel is running over TCP without encryption"
51+
52+
53+
class _DropTransportEncryptionNotice(logging.Filter):
54+
"""Drop ipykernel's unencrypted-TCP notice, and nothing else."""
55+
56+
def filter(self, record: logging.LogRecord) -> bool:
57+
"""Return False only for the one message this build does not need."""
58+
return _SUPPRESSED not in record.getMessage()
59+
60+
61+
# Traitlets names an Application's logger after its class.
62+
logging.getLogger("IPKernelApp").addFilter(_DropTransportEncryptionNotice())

‎pyproject.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ extend-allowed-calls = ["datafusion.lit", "lit"]
178178
]
179179
"docs/*" = ["D"]
180180
"docs/source/conf.py" = ["ANN001", "ERA001", "INP001"]
181+
# A Jupyter kernel config file, executed by traitlets rather than imported.
182+
"docs/ipython_kernel_config.py" = ["INP001"]
181183

182184
# CI and pre-commit invoke codespell with different paths, so we have a little
183185
# redundancy here, and we intentionally drop python in the path.

0 commit comments

Comments
 (0)