From 166791c67e72558196221fb10d8dc211c164f641 Mon Sep 17 00:00:00 2001 From: OutlyingWest Date: Wed, 4 Feb 2026 16:55:33 +0100 Subject: [PATCH 1/2] initial documentation --- .github/workflows/docs.yml | 57 +++++++++++++++++++++ .gitignore | 3 ++ docs/api/index.md | 54 ++++++++++++++++++++ docs/api/install.md | 17 +++++++ docs/api/kernel.md | 28 +++++++++++ docs/api/utilities.md | 18 +++++++ docs/getting-started/installation.md | 64 ++++++++++++++++++++++++ docs/getting-started/quickstart.md | 72 +++++++++++++++++++++++++++ docs/guides/magic-commands.md | 69 +++++++++++++++++++++++++ docs/guides/wrap-kernel.md | 23 +++++++++ docs/img/JUmPER01.png | Bin 0 -> 22204 bytes docs/img/JUmPER01.svg | 23 +++++++++ docs/index.md | 20 ++++++++ jumper_wrapper_kernel/install.py | 10 +++- jumper_wrapper_kernel/kernel.py | 11 ++++ jumper_wrapper_kernel/utilities.py | 14 ++++++ mkdocs.yml | 33 ++++++++++++ pyproject.toml | 5 ++ 18 files changed, 519 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/api/index.md create mode 100644 docs/api/install.md create mode 100644 docs/api/kernel.md create mode 100644 docs/api/utilities.md create mode 100644 docs/getting-started/installation.md create mode 100644 docs/getting-started/quickstart.md create mode 100644 docs/guides/magic-commands.md create mode 100644 docs/guides/wrap-kernel.md create mode 100644 docs/img/JUmPER01.png create mode 100644 docs/img/JUmPER01.svg create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..5abd31e --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,57 @@ +name: Docs + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: write + issues: write + pull-requests: write + +concurrency: + group: docs-pages + cancel-in-progress: true + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install documentation dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[docs] + + - name: Build + run: mkdocs build --clean + + - name: Publish to gh-pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_branch: gh-pages + publish_dir: ./site + destination_dir: ${{ github.event_name == 'push' && '' || format('pr-{0}', github.event.pull_request.number) }} + keep_files: true + + - name: Comment preview URL on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request.number; + const url = `https://${context.repo.owner}.github.io/${context.repo.repo}/pr-${pr}/`; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr, + body: `Docs preview: ${url}` + }); diff --git a/.gitignore b/.gitignore index a97e19e..e9eeb71 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ __pycache__/ dist/ build/ *.egg + +# Documentation +site/ diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..63829e4 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,54 @@ +--- +title: API Overview +--- + +# API Overview + +The Jumper Wrapper Kernel exposes a modular API organized into three main components: + +- **Kernel** - The main `JumperWrapperKernel` class that implements the Jupyter kernel protocol and manages kernel wrapping +- **Installation** - Functions for installing and uninstalling the kernel specification +- **Utilities** - Helper functions for magic command detection and cell routing + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Jupyter Frontend │ +└───────────────────────┬─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ JumperWrapperKernel │ +│ ┌─────────────────┐ ┌──────────────────────────────┐ │ +│ │ JumperWrapper │ │ jumper-extension │ │ +│ │ Magics │ │ (loaded locally) │ │ +│ │ - %list_kernels │ │ - %perfmonitor_start │ │ +│ │ - %wrap_kernel │ │ - %perfmonitor_stop │ │ +│ └─────────────────┘ │ - etc. │ │ +│ └──────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Wrapped Kernel │ │ +│ │ (Python, R, Julia, etc.) │ │ +│ │ - Receives forwarded code │ │ +│ │ - Returns execution results │ │ +│ └─────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +## Message Flow + +1. Cell code arrives at `do_execute()` +2. `_is_local_magic()` checks if code contains only local magic commands +3. Local magics are executed via `_execute_local_magic()` +4. Other code is forwarded via `_forward_to_wrapped_kernel()` +5. Pre/post cell events are triggered for jumper-extension hooks + +## Module Reference + +For detailed API documentation, see: + +- [Kernel](kernel.md) - Main kernel class and magic commands +- [Installation](install.md) - Kernel installation functions +- [Utilities](utilities.md) - Magic detection helpers diff --git a/docs/api/install.md b/docs/api/install.md new file mode 100644 index 0000000..4b54e22 --- /dev/null +++ b/docs/api/install.md @@ -0,0 +1,17 @@ +--- +title: Installation +--- + +# Installation Module + +The installation module provides functions for installing and uninstalling the Jumper Wrapper Kernel specification. + +## Functions + +::: jumper_wrapper_kernel.install + options: + show_root_heading: false + members: + - install_kernel + - uninstall_kernel + - main diff --git a/docs/api/kernel.md b/docs/api/kernel.md new file mode 100644 index 0000000..f4c221d --- /dev/null +++ b/docs/api/kernel.md @@ -0,0 +1,28 @@ +--- +title: Kernel +--- + +# Kernel Module + +The kernel module contains the main `JumperWrapperKernel` class and the `JumperWrapperMagics` class for magic command handling. + +## JumperWrapperMagics + +::: jumper_wrapper_kernel.kernel.JumperWrapperMagics + options: + show_root_heading: true + members: + - list_kernels + - wrap_kernel + +## JumperWrapperKernel + +::: jumper_wrapper_kernel.kernel.JumperWrapperKernel + options: + show_root_heading: true + members: + - do_execute + - do_shutdown + - do_complete + - do_inspect + - kernel_info diff --git a/docs/api/utilities.md b/docs/api/utilities.md new file mode 100644 index 0000000..c228577 --- /dev/null +++ b/docs/api/utilities.md @@ -0,0 +1,18 @@ +--- +title: Utilities +--- + +# Utilities Module + +The utilities module provides helper functions for detecting and classifying magic commands in cell content. + +## Functions + +::: jumper_wrapper_kernel.utilities + options: + show_root_heading: false + members: + - get_line_magics_cached + - is_known_line_magic + - is_pure_line_magic_cell + - is_local_magic_cell diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 0000000..942355a --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,64 @@ +--- +title: Installation +--- + +# Installation + +## Requirements + +- Python >= 3.8 +- ipykernel >= 6.0 +- jupyter_client >= 7.0 +- jumper-extension >= 0.3.0 + +## Standard Installation + +Install the package from PyPI: + +```bash +pip install jumper_wrapper_kernel +``` + +Then install the kernel specification: + +```bash +python -m jumper_wrapper_kernel.install install +``` + +## Installation for Virtual Environments + +If you're using a virtual environment or conda, install to `sys.prefix`: + +```bash +python -m jumper_wrapper_kernel.install install --sys-prefix +``` + +## Development Installation + +Clone the repository and install in editable mode: + +```bash +git clone https://github.com/ScaDS/jumper_wrapper_kernel.git +cd jumper_wrapper_kernel +pip install -e . +python -m jumper_wrapper_kernel.install install +``` + +## Verification + +After installation, verify the kernel is available: + +```bash +jupyter kernelspec list +``` + +You should see `jumper_wrapper` in the list of available kernels. + +## Uninstallation + +To remove the kernel: + +```bash +python -m jumper_wrapper_kernel.install uninstall +pip uninstall jumper_wrapper_kernel +``` diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md new file mode 100644 index 0000000..d5c55e1 --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,72 @@ +--- +title: Quickstart +--- + +# Quickstart + +This guide shows you how to use the Jumper Wrapper Kernel in a few simple steps. + +## Step 1: Select the Kernel + +1. Start Jupyter Notebook or JupyterLab +2. Create a new notebook +3. Select **Jumper Wrapper Kernel** as your kernel + +## Step 2: List Available Kernels + +See which kernels can be wrapped: + +```python +%list_kernels +``` + +Output: +``` +Available Jupyter Kernels: +-------------------------------------------------- + python3: Python 3 (ipykernel) (python) + ir: R (r) + julia-1.9: Julia 1.9 (julia) +-------------------------------------------------- +``` + +## Step 3: Wrap a Kernel + +Wrap your desired kernel: + +```python +%wrap_kernel python3 +``` + +Output: +``` +Successfully wrapped kernel: python3 +Hint: Refresh the page (without restarting the kernel) to enable syntax highlighting for the wrapped language. +``` + +## Step 4: Use Performance Monitoring + +Now you can use jumper-extension commands while running code on the wrapped kernel: + +```python +# Start monitoring (handled locally) +%perfmonitor_start + +# Run code on the wrapped kernel +import numpy as np +x = np.random.rand(1000, 1000) +y = np.dot(x, x.T) + +# View performance report (handled locally) +%perfmonitor_perfreport +``` + +## How It Works + +The Jumper Wrapper Kernel acts as a proxy: + +1. **Magic commands** from jumper-extension are intercepted and executed locally +2. **All other code** is forwarded to the wrapped kernel +3. **Output** is streamed back to the notebook + +This allows you to monitor performance of any Jupyter kernel, regardless of its language. diff --git a/docs/guides/magic-commands.md b/docs/guides/magic-commands.md new file mode 100644 index 0000000..649b38a --- /dev/null +++ b/docs/guides/magic-commands.md @@ -0,0 +1,69 @@ +--- +title: Magic Commands +--- + +# Magic Commands + +The Jumper Wrapper Kernel provides its own magic commands for kernel management, plus full access to jumper-extension magic commands for performance monitoring. + +## Wrapper Magic Commands + +### `%list_kernels` + +Lists all available Jupyter kernels that can be wrapped. + +```python +%list_kernels +``` + +Output: +``` +Available Jupyter Kernels: +-------------------------------------------------- + python3: Python 3 (ipykernel) (python) + ir: R (r) + julia-1.9: Julia 1.9 (julia) +-------------------------------------------------- +Currently wrapped kernel: python3 +``` + +### `%wrap_kernel` + +Wraps an existing Jupyter kernel. All subsequent code (except local magic commands) will be forwarded to this kernel. + +**Basic usage:** + +```python +%wrap_kernel python3 +``` + +**With permanent kernel spec:** + +```python +%wrap_kernel ir --save jumper-r +``` + +This creates a new kernel spec `jumper-r` that automatically wraps the R kernel on startup. + +## Jumper Extension Commands + +All jumper-extension magic commands are available and executed locally: + +| Command | Description | +|---------|-------------| +| `%perfmonitor_start [interval]` | Start performance monitoring | +| `%perfmonitor_stop` | Stop performance monitoring | +| `%perfmonitor_perfreport` | View performance report | +| `%perfmonitor_plot` | Plot performance data | +| `%cell_history` | View cell execution history | + +For complete documentation on jumper-extension commands, see the [jumper-extension documentation](https://scads.github.io/jumper_ipython_extension/). + +## Command Routing + +The kernel automatically routes commands: + +- **Local execution**: Wrapper magics and jumper-extension magics +- **Forwarded to wrapped kernel**: All other code + +This routing is determined by analyzing the cell content before execution. diff --git a/docs/guides/wrap-kernel.md b/docs/guides/wrap-kernel.md new file mode 100644 index 0000000..70f5d38 --- /dev/null +++ b/docs/guides/wrap-kernel.md @@ -0,0 +1,23 @@ +# How to Wrap a new Kernel + +=== "Presentation" + + +=== "Video" + \ No newline at end of file diff --git a/docs/img/JUmPER01.png b/docs/img/JUmPER01.png new file mode 100644 index 0000000000000000000000000000000000000000..5a98ca873501f40a07835a78918e15496fe06ab2 GIT binary patch literal 22204 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 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..cc523c0 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,20 @@ +--- +title: Jumper Wrapper Kernel Documentation +--- + +# Jumper Wrapper Kernel + +

+ JUmPER logo +

+ +Welcome to the documentation for the Jumper Wrapper Kernel, a Jupyter kernel that wraps other kernels while providing jumper-extension performance monitoring capabilities. + +Jumper Wrapper Kernel lets you: + +- **Wrap any Jupyter kernel** (Python, R, Julia, etc.) with a single magic command +- **Monitor performance** of any wrapped kernel using jumper-extension magic commands +- **Forward code transparently** to the wrapped kernel while keeping monitoring local +- **Create permanent kernel specs** that auto-wrap specific kernels on startup + +To get started quickly, follow the steps in the [Installation](getting-started/installation.md) and [Quickstart](getting-started/quickstart.md) guides. For detailed command descriptions and programmatic usage, refer to the [API Reference](api/index.md) section. diff --git a/jumper_wrapper_kernel/install.py b/jumper_wrapper_kernel/install.py index 53623d7..802ca93 100644 --- a/jumper_wrapper_kernel/install.py +++ b/jumper_wrapper_kernel/install.py @@ -20,7 +20,12 @@ def install_kernel(user=True, prefix=None): - """Install the kernel spec.""" + """Install the Jumper Wrapper Kernel specification. + + Args: + user: If True, install for current user only. Ignored if prefix is set. + prefix: Install to specific prefix path (e.g., sys.prefix for virtualenv). + """ import tempfile import shutil @@ -52,7 +57,7 @@ def install_kernel(user=True, prefix=None): def uninstall_kernel(): - """Uninstall the kernel spec.""" + """Remove the Jumper Wrapper Kernel specification.""" kernel_spec_manager = KernelSpecManager() try: @@ -64,6 +69,7 @@ def uninstall_kernel(): def main(): + """CLI entry point for kernel installation and removal.""" parser = argparse.ArgumentParser(description='Install/Uninstall Jumper Wrapper Kernel') parser.add_argument('action', choices=['install', 'uninstall'], help='Action to perform') parser.add_argument('--user', action='store_true', default=True, diff --git a/jumper_wrapper_kernel/kernel.py b/jumper_wrapper_kernel/kernel.py index d1e0528..c56a6de 100644 --- a/jumper_wrapper_kernel/kernel.py +++ b/jumper_wrapper_kernel/kernel.py @@ -32,6 +32,12 @@ class JumperWrapperMagics(Magics): """Magic commands for the Jumper Wrapper Kernel.""" def __init__(self, shell, kernel): + """Initialize magic commands with shell and kernel references. + + Args: + shell: IPython shell instance. + kernel: JumperWrapperKernel instance for delegating operations. + """ super().__init__(shell) self._kernel = kernel @@ -78,6 +84,11 @@ class JumperWrapperKernel(IPythonKernel): help="Kernel name to automatically wrap on startup. If empty, no auto-wrap.") def __init__(self, **kwargs): + """Initialize the wrapper kernel. + + Loads jumper-extension, registers wrapper magics, and prepares + for optional auto-wrapping based on configuration. + """ super().__init__(**kwargs) # Wrapped kernel state diff --git a/jumper_wrapper_kernel/utilities.py b/jumper_wrapper_kernel/utilities.py index f065e0b..2c96635 100644 --- a/jumper_wrapper_kernel/utilities.py +++ b/jumper_wrapper_kernel/utilities.py @@ -8,11 +8,25 @@ @lru_cache(maxsize=1) def get_line_magics_cached() -> FrozenSet[str]: + """Return cached set of all registered line magic names. + + Returns: + Frozen set of magic command names (without % prefix). + """ ip = get_ipython() return frozenset(ip.magics_manager.lsmagic().get("line", [])) def is_known_line_magic(line: str, line_magics: frozenset) -> bool: + """Check if a line starts with a known magic command. + + Args: + line: Single line of code to check. + line_magics: Set of known magic names (without % prefix). + + Returns: + True if line starts with %, False otherwise. + """ s = line.lstrip() if not s.startswith("%"): return False diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..a8f85be --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,33 @@ +site_name: Jumper Wrapper Kernel +site_description: A Jupyter kernel that wraps other kernels with jumper-extension support +repo_url: https://github.com/ScaDS/jumper_wrapper_kernel +repo_name: ScaDS/jumper_wrapper_kernel + +docs_dir: docs + +theme: + name: material + logo: img/JUmPER01.svg + +nav: + - Home: index.md + - Getting started: + - Installation: getting-started/installation.md + - Quickstart: getting-started/quickstart.md + - User Guide: + - Magic Commands: guides/magic-commands.md + - Auto-Wrap Kernels: guides/wrap-kernel.md + - API Reference: + - Overview: api/index.md + - Kernel: api/kernel.md + - Installation: api/install.md + - Utilities: api/utilities.md + +markdown_extensions: + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + +plugins: + - search + - mkdocstrings diff --git a/pyproject.toml b/pyproject.toml index 22e6f76..dc6ddda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,11 @@ dev = [ "pytest-cov", ] +docs = [ + "mkdocs-material", + "mkdocstrings[python]", +] + [project.urls] Homepage = "https://github.com/ScaDS/jumper_wrapper_kernel" Repository = "https://github.com/ScaDS/jumper_wrapper_kernel" From 0274205d4ac896092f18c6b8ad3352562e5008b3 Mon Sep 17 00:00:00 2001 From: OutlyingWest Date: Thu, 5 Feb 2026 11:36:59 +0100 Subject: [PATCH 2/2] architecture chapter stub --- docs/api/index.md | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/docs/api/index.md b/docs/api/index.md index 63829e4..65d76a2 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -11,31 +11,7 @@ The Jumper Wrapper Kernel exposes a modular API organized into three main compon - **Utilities** - Helper functions for magic command detection and cell routing ## Architecture - -``` -┌─────────────────────────────────────────────────────────┐ -│ Jupyter Frontend │ -└───────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────┐ -│ JumperWrapperKernel │ -│ ┌─────────────────┐ ┌──────────────────────────────┐ │ -│ │ JumperWrapper │ │ jumper-extension │ │ -│ │ Magics │ │ (loaded locally) │ │ -│ │ - %list_kernels │ │ - %perfmonitor_start │ │ -│ │ - %wrap_kernel │ │ - %perfmonitor_stop │ │ -│ └─────────────────┘ │ - etc. │ │ -│ └──────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ Wrapped Kernel │ │ -│ │ (Python, R, Julia, etc.) │ │ -│ │ - Receives forwarded code │ │ -│ │ - Returns execution results │ │ -│ └─────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ -``` +To be described ## Message Flow