This guide is for contributors working on ipymini. It consolidates architecture, style, testing, PR/release flow, and onboarding notes.
- Small, readable, testable kernel with strong IPython parity.
- Prefer IPython APIs over re‑implementing Python semantics.
- Match ipykernel behavior where it matters (message shapes, ordering, history, interrupts, etc.).
- Avoid complexity (e.g traitlets) where possible
- Modernize (e.g no more tornado)
IPYMINI_STOP_ON_ERROR_TIMEOUT: seconds to keep aborting queued executes after an error (default 0.0).IPYMINI_USE_JEDI=0|1– override IPython’s Jedi setting.IPYMINI_CELL_NAME– override debug cell filename.
Run non-slow tests:
pytest -q
Run everything (including slow tests):
tools/run_tests.sh
Note: tools/run_tests.sh already runs pytest -q, so skip running it beforehand or you'll run tests twice. To run only slow tests, use:
pytest -q -m slow
Run tests for a specific module:
pytest tests/debug/ # debug module tests
pytest tests/shell/ # shell module tests
pytest tests/term/ # term module tests
pytest tests/zmqthread/ # zmqthread module tests
pytest tests/kernel/ # kernel integration tests
Notes:
- Tests start the kernel in a separate process (via
KernelManager). - Ensure
JUPYTER_PATHincludesshare/jupyterfrom this repo for tests. - Debug tests require
debugpy(declared in test extras).
- Prefer protocol‑level tests using
KernelManagerand helpers intests/kernel_utils.py. - Use
KernelHarness(fixture intests/conftest.py) for minimal, readable protocol‑script tests. - Avoid large sleeps and long timeouts; use monotonic timeouts and explicit status waits.
- For blocking debugger behavior, always use a separate process.
We follow the fastai style guide (style.md):
- Favor brevity; one‑liners for single statements (including
if/for/try/with). - Avoid vertical whitespace; wrap at ~140 chars.
- No semicolons for chaining statements.
- Dicts with 3+ identifier keys use
dict(...). - Avoid type annotations on LHS variables (dataclasses excepted).
Run chkstyle before committing - it will look for clear style violations.
Use the repo script (GitHub CLI required):
tools/pr.sh "Message" [label] [body|body-file]
Notes:
- The script creates a branch, commits tracked changes, opens a PR, and merges.
- Ensure the working tree is clean and all intended files are staged.
- Normal releases use fastship:
ship-gh
ship-pypi
ship-bump
ipymini/kernel.py– protocol, ZMQ routing, subshells.ipymini/shell/shell.py– IPython integration and output capture.tests/– protocol expectations and integration behavior (organized by module).
Core flow:
MiniKernelowns sockets, threads, and dispatch.microioprovides the small concurrency primitives used for service lifecycle state, task ownership, thread readiness, thread-bound channels, and request waiters.SubshellManagermanages the parent subshell (main thread) and optional child subshells (worker threads) sharing a user namespace.MiniShellwraps IPython: execute, display, history, comms, debugger.
Key files:
ipymini/kernel.py– Jupyter protocol, ZMQ router loops, subshells.ipymini/shell/shell.py– IPython integration, execution, output capture, debug.ipymini/debug/– debugger integration (DAP + debugpy).ipymini/term/– stream capture and IPython display hooks.ipymini/zmqthread/– ZMQ thread helpers (router, iopub, heartbeat).ipymini/__main__.py– CLI entry, install helper.tests/– protocol and behavioral tests (organized by module).
MiniKernelcreates aSubshellManagerwhich createsSubshellinstances.- Each
Subshellruns amicroio.ActorCoremailbox loop; the parent and child subshells share the same serialized message handling path, but use different runners. - Each
Subshellholds aMiniShell(self.shell), which wraps an IPythonInteractiveShell(self.shell.ipy). Subshell.__init__receives theMiniKernelaskerneland stores it viastore_attr.MiniKernel.shellis a shortcut toself.subshells.parent.shell(the parent subshell'sMiniShell).- Child subshells (created via
SubshellManager.create) share the sameuser_nsdict but get their ownMiniShelland IPython instance. get_ipython()returnsMiniShell.ipy(theInteractiveShellsingleton for the parent, non-singleton for children).
- The parent subshell runs in the main thread, so SIGINT can interrupt running code without killing the kernel when idle.
- Each subshell has a persistent asyncio loop; code runs while the loop is running, so
asyncio.create_task(...)works in sync cells. - Output routing uses contextvars to associate streams/displays with the current parent message (works across tasks and user-launched threads).
- On POSIX, kernel startup isolates the kernel into its own process group; shutdown and direct
SIGTERMterminate that group as the final step. Kernels launched byKernelManageralso watchJPY_PARENT_PID, so nested ipymini kernels shut down when their launcher exits and then reap their own process group. ipymini does not try to gracefully cancel arbitrary user-created threads/processes individually. Windows currently skips this process-group teardown. - On POSIX,
SIGUSR1dumps all Python thread stacks viafaulthandlerfor stuck-kernel diagnostics.
- Shell/control ROUTER sockets run in background threads via
AsyncRouterThread. - Kernel-owned service threads use
microio.ServiceThread/LoopServiceThread, so startup failures are visible and join timeouts are checked. LoopServiceThreadruns its async service body inside amicroio.TaskGroup; stopping the thread cancels owned async children as normal shutdown.MiniKernelusesmicroio.ServiceGroupfor kernel-owned thread start/wait/stop/join boilerplate.- The debugpy reader is also a
ServiceThread, so pending debug requests are failed on reader crash/close and joins are checked. - The router thread is the only thread that touches its socket (thread‑safety).
- Outbound replies are enqueued; the router loop drains the queue after inbound messages to avoid starvation.
- Async sockets use
zmq.asyncio.Context.shadow(self.context)to avoid multiple ZMQ contexts. - Synchronous ZMQ service loops share
zmqthread.polling.poll_in()for the common poll-for-readable pattern.
interrupt_requestsends SIGINT and also attempts task cancellation for async cells.- Subshell execution owns a
microio.CancelScopethat is active only while awaiting an async IPython cell; child-subshell async cells are cancelled through that scope, while sync cells still use thread/SIGINT interruption. - Cancelled async tasks are translated into
KeyboardInterruptfor parity. - Interrupts cancel pending stdin waits and emit IOPub error messages.
shutdown_requestis idempotent once stopping begins; other control requests during stopping returnKernelStopping.
execute_inputis emitted before any live stream/display output.- Streams and display data are live when a sender is configured; otherwise they buffer and flush after execution.
- Inbound comm open/msg/close are routed to the
commpackage'sCommManager(reachable asget_ipython().kernel.comm_manager), which drives the registered target/on_msg/on_closecallbacks; inbound messages are not echoed back on IOPub. - Outbound comms (
comm.create_comm(...).send(...), callback replies) publish on IOPub viaIpyminiComm.publish_msg, parented tokernel.current_parent()— the sharedparent_header_varfor the active cell/comm-handler/spawned-thread, falling back to the kernel's last parent. The comm layer is bound to the kernel at startup viaset_kernel(buffers preserved).
- History uses IPython’s
HistoryManagerfor tail/search/range. - inspect/complete/is_complete are delegated to IPython (bridge methods).
- IPython config/extensions/startup scripts are loaded via an
InteractiveShellAppwrapper. - The test
tests/test_ipython_startup_integration.pyexercises:ipython_kernel_config.pyprofile_default/startup/*.py
InteractiveShell.display_pagecontrols whether pager output is emitted asdisplay_data(True) or reply payloads (False).
NB: meta/ is not commited to git -- it is used for code reviews, timing details, etc.