Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to run in fully automated mode #2

Merged
merged 18 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading