Skip to content

Commit

Permalink
checks
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep committed Nov 29, 2023
1 parent 4421605 commit a064ad3
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 43 deletions.
18 changes: 18 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ features = ["test"]
[tool.hatch.envs.test.scripts]
run = "pytest -vv {args}"

[tool.ruff]
select = ["ALL"]
extend-ignore = [
"COM812", "ISC001", # Incompatible with formatter
"D203", # Use no blank line before class
"D213", # Use multiline summary on first line
"FIX002", # Comments with “TODO” are OK
"TD002", # No need for TODO authors
]
allowed-confusables = ["", "×"]
[tool.ruff.extend-per-file-ignores]
"tests/**/*.py" = [
"D100", # No module docstrings necessary for tests
"D103", # No function docstrings necessary for tests
"INP001", # Pytest directories are not packages
"S101", # Pytest rewrites asserts
]

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
1 change: 1 addition & 0 deletions src/prelude_runner/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Prelude Runner."""
from .core import Preludes, execute

__all__ = ["execute", "Preludes"]
22 changes: 16 additions & 6 deletions src/prelude_runner/cli.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,43 @@
"""Command line parsing and execution."""
from __future__ import annotations

from argparse import ArgumentParser
from pathlib import Path
from typing import Protocol
from typing import TYPE_CHECKING, Protocol, Sequence

import nbformat

from .core import Preludes, execute
from .types import Notebook

if TYPE_CHECKING:
from .types import Notebook


def load_preludes(d: Path) -> Preludes:
"""Load prelude code from config directory."""
return Preludes(
notebook=(d / "prelude_notebook.py").read_text(),
cell=(d / "prelude_cell.py").read_text(),
)


class Args(Protocol):
"""Parsed command line arguments."""

preludes: Path
nb_path: Path


def parse_args(argv: list[str] | None = None) -> Args:
parser = ArgumentParser(argv)
def parse_args(argv: Sequence[str] | None = None) -> Args:
"""Parse command line arguments."""
parser = ArgumentParser()
parser.add_argument("--preludes", type=Path, help="Path to prelude directory")
parser.add_argument("nb-path", type=Path, help="Path to notebook directory")
return parser.parse_args()
return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> None:
def main(argv: Sequence[str] | None = None) -> None:
"""Execute main entry point."""
args = parse_args(argv)
preludes = load_preludes(args.preludes)
for nb_path in args.nb_path.rglob("*.ipynb"):
Expand Down
35 changes: 24 additions & 11 deletions src/prelude_runner/core.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
"""Main exports."""
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING, Any

from jupyter_client.manager import KernelManager
from nbclient import NotebookClient
from nbclient.util import ensure_async

from .types import CodeCell, Notebook
if TYPE_CHECKING:
from pathlib import Path

from jupyter_client.manager import KernelManager

from .types import CodeCell, Notebook


@dataclass
class Preludes:
"""Prelude code to execute before notebook and cell."""

notebook: str | None = None
cell: str | None = None


def add_reproducible_hooks(client: NotebookClient, preludes: Preludes) -> None:
def add_prelude_hooks(client: NotebookClient, preludes: Preludes) -> None:
"""Add hooks for prelude execution to NotebookClient."""

async def execute(source: str) -> None:
await ensure_async(
client.kc.execute(
source, silent=True, store_history=False, allow_stdin=False
)
source,
silent=True,
store_history=False,
allow_stdin=False,
),
)

async def on_notebook_start(notebook: Notebook) -> None:
async def on_notebook_start(notebook: Notebook) -> None: # noqa: ARG001
await execute(preludes.notebook)

async def on_cell_execute(cell: CodeCell, cell_index: int) -> None:
async def on_cell_execute(cell: CodeCell, cell_index: int) -> None: # noqa: ARG001
await execute(preludes.cell)

client.on_notebook_start = on_notebook_start
Expand All @@ -39,12 +52,12 @@ def execute(
*,
cwd: Path | None = None,
km: KernelManager | None = None,
**kwargs: Any,
**kwargs: Any, # noqa: ANN401
) -> Notebook:
"""Execute a notebook's code, updating outputs within the notebook object."""
resources = {}
if cwd is not None:
resources["metadata"] = {"path": cwd}
client = NotebookClient(nb=nb, resources=resources, km=km, **kwargs)
add_reproducible_hooks(client, preludes)
add_prelude_hooks(client, preludes)
return client.execute()
26 changes: 24 additions & 2 deletions src/prelude_runner/types.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,60 @@
"""Types more specific than NotebookNode."""
from __future__ import annotations

from typing import Any, Literal, Protocol, TypedDict


class CellMetadata(TypedDict):
"""Common metadata for all cells."""

tags: list[str]


class Cell(Protocol):
"""A cell in a notebook."""

cell_type: Literal["markdown", "code"]
metadata: CellMetadata
source: str


class Output(Protocol):
"""A code cell’s output."""

output_type: Literal[
"stream", "display_data", "execute_result", "error", "update_display_data"
"stream",
"display_data",
"execute_result",
"error",
"update_display_data",
]
data: dict[str, str]


class Stream(Protocol):
"""One type of output from a code cell."""

output_type: Literal["stream"]
name: Literal["stdout", "stderr"]
text: str


class CodeCell(Cell):
"""A code cell."""

cell_type: Literal["code"]
outputs: list[Stream | Any] # TODO
# TODO: add other output types # noqa: TD003
outputs: list[Stream | Any]


class NotebookMetadata(TypedDict):
"""Common metadata for all notebooks."""

language_info: dict[str, str]


class Notebook(Protocol):
"""A Jupyter notebook."""

metadata: NotebookMetadata
cells: list[Cell]
9 changes: 9 additions & 0 deletions tests/data/config/prelude_cell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import random
from contextlib import suppress

random.seed(0)

with suppress(Exception):
from numpy.random import seed

seed(0) # noqa: NPY002
File renamed without changes.
Empty file.
16 changes: 0 additions & 16 deletions tests/example-config/prelude_cell.py

This file was deleted.

26 changes: 18 additions & 8 deletions tests/test_runner.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,46 @@
from pathlib import Path
from typing import TYPE_CHECKING

import nbclient
import pytest
from nbclient.exceptions import CellExecutionError
from nbformat import v4
from prelude_runner.cli import load_preludes
from prelude_runner.cli import load_preludes, main
from prelude_runner.core import Preludes, execute
from prelude_runner.types import CodeCell, Notebook

if TYPE_CHECKING:
from prelude_runner.types import CodeCell, Notebook

tests_dir = Path(__file__).parent


@pytest.fixture(scope="session")
def preludes():
return load_preludes(tests_dir / "example-config")
def preludes() -> Preludes:
return load_preludes(tests_dir / "data/config")


@pytest.mark.parametrize(
("code", "expected"),
[
pytest.param("import random; print(random.randint(0, 10))", "6", id="stdlib"),
pytest.param(
"import numpy as np; print(np.random.randint(0, 10))", "5", id="numpy"
"import numpy as np; print(np.random.randint(0, 10))",
"5",
id="numpy",
),
],
)
def test_execute(preludes: Preludes, code: str, expected: str):
def test_execute(preludes: Preludes, code: str, expected: str) -> None:
cell: CodeCell = v4.new_code_cell(cell_type="code", source=code)
nb: Notebook = v4.new_notebook(cells=[cell])
execute(nb, preludes)
assert cell.outputs[0].text.strip() == expected


def test_traceback_intact(preludes: Preludes):
def test_traceback_intact(preludes: Preludes) -> None:
"""Tests that the traceback reports the same line and cell numbers."""

def mk_nb():
def mk_nb() -> None:
cell: CodeCell = v4.new_code_cell(cell_type="code", source="1/0")
nb: Notebook = v4.new_notebook(cells=[cell])
return nb
Expand All @@ -45,3 +50,8 @@ def mk_nb():
with pytest.raises(CellExecutionError) as exc_orig:
nbclient.execute(mk_nb())
assert exc_rr.value.traceback == exc_orig.value.traceback


def test_cli() -> None:
nb_path = tests_dir / "data/notebooks"
main([nb_path])

0 comments on commit a064ad3

Please sign in to comment.