Skip to content

Commit

Permalink
Merge pull request #2 from glue-viz/fully-automated
Browse files Browse the repository at this point in the history
Add ability to run in fully automated mode
  • Loading branch information
astrofrog authored Nov 13, 2024
2 parents 824358b + 0e40a53 commit 937257c
Show file tree
Hide file tree
Showing 19 changed files with 556 additions and 112 deletions.
15 changes: 15 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: "github-actions" # See documentation for possible values
directory: ".github/workflows" # Location of package manifests
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "*"
22 changes: 22 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Automated test

on:
workflow_dispatch:
pull_request:
push:

jobs:
test:
uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1
with:
envs: |
- linux: py310-test
- linux: py311-test
- linux: py312-test
- linux: py313-test
- macos: py310-test
- macos: py311-test
- macos: py312-test
- macos: py313-test
- windows: py312-test
- windows: py313-test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ __pycache__
dist
build
.ipynb_checkpoints
__pycache__
29 changes: 29 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
ci:
autofix_prs: false
autoupdate_schedule: 'monthly'

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-added-large-files
args: ["--enforce-all", "--maxkb=300"]
- id: check-case-conflict
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: check-xml
- id: check-yaml
exclude: ".*(.github.*)$"
- id: detect-private-key
- id: end-of-file-fixer
exclude: ".*(data.*|extern.*|licenses.*|_static.*|_parsetab.py)$"
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.3.4"
hooks:
- id: ruff
args: ["--fix", "--show-fixes"]
- id: ruff-format
89 changes: 38 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,53 @@
This repository contains an experimental utility to monitor the visual output of
cells from Jupyter notebooks.

## Requirements

On the machine being used to run the ``monitor_cells.py``:
## Installing

* [numpy](https://numpy.org)
* [click](https://click.palletsprojects.com/en/stable/)
* [pillow](https://python-pillow.org/)
* [playwright](https://pypi.org/project/playwright/)
To install, check out this repository and:

On the Jupyter Lab server, optionally (but recommended):
pip install -e .

* [jupyter-collaboration](https://github.com/jupyterlab/jupyter-collaboration)
Python 3.10 or later is supported (Python 3.12 or later on Windows).

If this is the first time using playwright, you will need to run::
If this is the first time using playwright, you will also need to run:

playwright install firefox

## Installing
## Quick start

To install, check out this repository and:
First, write one or more blocks of code you want to benchmark each in a cell. In
addition, as early as possible in the notebook, make sure you set the border
color on any ipywidget layout you want to record:

pip install -e .
widget.layout.border = '1px solid rgb(143, 56, 3)'

The R and G values should be kept as (143, 56), and the B color should be unique for each widget and be a value between 0 and 255 (inclusive).

Then, to run the notebook and monitor the changes in widget output, run:

jupyter-output-monitor --notebook mynotebook.ipynb

Where ``mynotebook.ipynb`` is the name of your notebook. By default, this will
open a window showing you what is happening, but you can also pass ``--headless``
to run in headless mode.

## Using this on a remote Jupyter Lab instance

If you want to test this on an existing Jupyter Lab instance, including
remote ones, you can use ``--url`` instead of ``--notebook``:

jupyter-output-monitor http://localhost:8987/lab/tree/notebook.ipynb?token=7bb9a...

Note that the URL should include the path to the notebook, and will likely
require the token too.

You should make sure that all output cells in the notebook have been cleared
before running the above command, and that the widget border color has been
set as mention in the **Quick start** guide above.

If you make use of the [jupyter-collaboration](https://github.com/jupyterlab/jupyter-collaboration) plugin on the Jupyter Lab server, you will be able to
more easily e.g. clear the output between runs and edit the notebook in
between runs of ``jupyter-output-monitor``.

## How this works

Expand Down Expand Up @@ -83,46 +108,8 @@ and if using jdaviz:
To stop recording output for a given cell, you can set the border attribute to
``''``.

## Headless vs non-headless mode

By default, the script will open up a window and show what it is doing. It will
also wait until it detects any input cells before proceeding. This then gives
you the opportunity to enter any required passwords, and open the correct
notebook. However, note that if Jupyter Lab opens up with a different notebook
to the one you want by default, it will start executing that one! It's also
better if the notebook starts off with output cells cleared, otherwise the script
may start taking screenshots straight away.

The easiest way to ensure that the correct notebook gets executed and that it
has had its output cells cleared is to make use of the
[jupyter-collaboration](https://github.com/jupyterlab/jupyter-collaboration)
plugin. With this plugin installed, you can open Jupyter Lab in a regular browser window,
and set it up so that the correct notebook is open by default and has its cells cleared,
and you can then launch the monitoring script. In fact, if you do this you can then
also run the script in headless mode since you know it should be doing the right thing.

One final note is that to avoid any jumping up and down of the notebook during
execution, the window opened by the script has a very large height so that the
full notebook fits inside the window without scrolling.

## How to use

* Assuming you have installed
[jupyter-collaboration](https://github.com/jupyterlab/jupyter-collaboration),
start up Jupyter Lab instance on a regular browser and go to the notebook you
want to profile.
* If not already done, write one or more blocks of code you want to benchmark
each in a cell. In addition, as early as possible in the notebook, make sure
you set the border color on any ipywidget layout you want to record.
* Make sure the notebook you want to profile is the main one opened and that
you have cleared any output cells.
* Run the main command in this package, specifying the URL to connect to for Jupyter Lab, e.g.:

jupyter-output-monitor http://localhost:8987

## Settings


### Headless

To run in headless mode, include ``--headless``
Expand Down
2 changes: 2 additions & 0 deletions jupyter_output_monitor/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from ._monitor import monitor
from ._version import __version__

__all__ = ["monitor", "__version__"]
4 changes: 4 additions & 0 deletions jupyter_output_monitor/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from ._monitor import monitor

if __name__ == "__main__":
monitor()
Binary file not shown.
Binary file not shown.
Binary file not shown.
107 changes: 107 additions & 0 deletions jupyter_output_monitor/_convert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Experimental command to convert a notebook to a script that tests widget
# output with solara but without launching a Jupyter Lab instance. Not yet
# exposed via the public API.

import os
from textwrap import indent

import click
import nbformat


def remove_magics(source):
lines = [line for line in source.splitlines() if not line.startswith("%")]
return os.linesep.join(lines)


def remove_excludes(source):
lines = [
line for line in source.splitlines() if not line.strip().endswith("# EXCLUDE")
]
return os.linesep.join(lines)


HEADER = """
import time
import solara
import playwright
import pytest_playwright
from IPython.display import display
def watch_screenshot(widget):
display_start = time.time()
last_change_time = display_start
last_screenshot = None
while time.time() - display_start < 5:
screenshot_bytes = widget.screenshot()
if screenshot_bytes != last_screenshot:
last_screenshot = screenshot_bytes
last_change_time = time.time()
return last_screenshot, last_change_time - display_start
def test_main(page_session, solara_test):
"""

DISPLAY_CODE = """
object_to_capture.add_class("test-object")
display(object_to_capture)
captured_object = page_session.locator(".test-object")
captured_object.wait_for()
"""

PROFILING_CODE = """
last_screenshot, time_elapsed = watch_screenshot(captured_object)
print(f"Extra time waiting for display to update: {time_elapsed:.2f}s")
"""


@click.command()
@click.argument("input_notebook")
@click.argument("output_script")
def convert(input_notebook, output_script):
nb = nbformat.read(input_notebook, as_version=4)

with open(output_script, "w") as f:
f.write(HEADER)

captured = False

for icell, cell in enumerate(nb["cells"]):
if cell.cell_type == "markdown":
f.write(indent(cell.source, " # ") + "\n\n")
elif cell.cell_type == "code":
if cell.source.strip() == "":
continue

lines = cell.source.splitlines()

new_lines = []

new_lines.append("cell_start = time.time()\n\n")

for line in lines:
if line.startswith("%") or line.strip().endswith("# EXCLUDE"):
continue
elif line.endswith("# SCREENSHOT"):
new_lines.append("object_to_capture = " + line)
new_lines.extend(DISPLAY_CODE.splitlines())
captured = True
else:
new_lines.append(line)

new_lines.append("cell_end = time.time()\n")
new_lines.append(
f'print(f"Cell {icell:2d} Python code executed in {{cell_end - cell_start:.2f}}s")',
)

if captured:
new_lines.extend(PROFILING_CODE.splitlines())

source = os.linesep.join(new_lines)

f.write(indent(source, " ") + "\n\n")


if __name__ == "__main__":
convert()
Loading

0 comments on commit 937257c

Please sign in to comment.