From 84d63713dc695880d7d544fa50760c811931b20e Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 10 Nov 2025 14:30:24 +0000 Subject: [PATCH 1/8] initial tests --- tests/test_tutorials.py | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/test_tutorials.py diff --git a/tests/test_tutorials.py b/tests/test_tutorials.py new file mode 100644 index 000000000..dd43c403c --- /dev/null +++ b/tests/test_tutorials.py @@ -0,0 +1,58 @@ +from pathlib import Path + +import pytest +from nbclient import NotebookClient +from nbclient.exceptions import CellExecutionError + +import nbformat +import nbclient + + +def _find_tutorial_notebooks(): + """Return a sorted list of notebook Paths under doc/tutorials. + + Skips the entire module if the tutorials directory does not exist. + """ + root = Path(__file__).resolve().parents[1] / "doc" / "tutorials" + if not root.exists(): + pytest.skip(f"Tutorials folder not found: {root}") + notebooks = sorted(root.rglob("*.ipynb")) + if not notebooks: + pytest.skip(f"No tutorial notebooks found in: {root}") + return notebooks + + +# Discover notebooks at import time so pytest can parametrize them. +_NOTEBOOKS = _find_tutorial_notebooks() + + +@pytest.mark.parametrize("nb_path", _NOTEBOOKS, ids=[p.name for p in _NOTEBOOKS]) +def test_execute_notebook(nb_path): + """Execute a single Jupyter notebook and fail if any cell errors occur. + + This uses nbclient.NotebookClient to run the notebook in its parent directory + so relative paths within the notebook resolve correctly. + """ + nb = nbformat.read(str(nb_path), as_version=4) + + # Prefer the notebook's kernelspec if provided, otherwise let nbclient pick the default. + kernel_name = nb.metadata.get("kernelspec", {}).get("name") + + client = NotebookClient( + nb, + timeout=600, + kernel_name=kernel_name, + resources={"metadata": {"path": str(nb_path.parent)}}, + ) + + try: + client.execute() + except CellExecutionError as exc: + # Re-raise with more context so pytest shows which notebook failed. + raise AssertionError(f"Error while executing notebook {nb_path}: {exc}") from exc + finally: + # Ensure kernel is shut down. + try: + client.shutdown_kernel() + except Exception: + pass From de62361bdf98372ed84d39fbf7e85ae8c33d053a Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 10 Nov 2025 14:30:45 +0000 Subject: [PATCH 2/8] remove old userguide tests --- tests/test_userguide.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 tests/test_userguide.py diff --git a/tests/test_userguide.py b/tests/test_userguide.py deleted file mode 100644 index d86547ae6..000000000 --- a/tests/test_userguide.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Tests inspired by examples in the user guide.""" -import pygambit as gbt - - -def test_trust_game(): - """Build the one-shot trust game from Kreps (1990)""" - g = gbt.Game.new_tree(players=["Buyer", "Seller"], - title="One-shot trust game, after Kreps (1990)") - g.append_move(g.root, "Buyer", ["Trust", "Not trust"]) - g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"]) - g.set_outcome(g.root.children[0].children[0], - g.add_outcome([1, 1], label="Trustworthy")) - g.set_outcome(g.root.children[0].children[1], - g.add_outcome([-1, 2], label="Untrustworthy")) - g.set_outcome(g.root.children[1], - g.add_outcome([0, 0], label="Opt-out")) - - -def test_myerson_poker(): - """Build the one-card poker example adapted from Myerson (1991)""" - g = gbt.Game.new_tree(players=["Alice", "Bob"], - title="One card poker game, after Myerson (1991)") - g.append_move(g.root, g.players.chance, ["King", "Queen"]) - for node in g.root.children: - g.append_move(node, "Alice", ["Raise", "Fold"]) - g.append_move(g.root.children[0].children[0], "Bob", ["Meet", "Pass"]) - g.append_infoset(g.root.children[1].children[0], - g.root.children[0].children[0].infoset) - alice_winsbig = g.add_outcome([2, -2], label="Alice wins big") - alice_wins = g.add_outcome([1, -1], label="Alice wins") - bob_winsbig = g.add_outcome([-2, 2], label="Bob wins big") - bob_wins = g.add_outcome([-1, 1], label="Bob wins") - g.set_outcome(g.root.children[0].children[0].children[0], alice_winsbig) - g.set_outcome(g.root.children[0].children[0].children[1], alice_wins) - g.set_outcome(g.root.children[0].children[1], bob_wins) - g.set_outcome(g.root.children[1].children[0].children[0], bob_winsbig) - g.set_outcome(g.root.children[1].children[0].children[1], alice_wins) - g.set_outcome(g.root.children[1].children[1], bob_wins) From 08ca752a7ca813defd2491cf87c4ff95a49cfedb Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 10 Nov 2025 14:34:00 +0000 Subject: [PATCH 3/8] organise imports --- tests/test_tutorials.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_tutorials.py b/tests/test_tutorials.py index dd43c403c..ea96dbf34 100644 --- a/tests/test_tutorials.py +++ b/tests/test_tutorials.py @@ -1,12 +1,10 @@ from pathlib import Path +import nbformat import pytest from nbclient import NotebookClient from nbclient.exceptions import CellExecutionError -import nbformat -import nbclient - def _find_tutorial_notebooks(): """Return a sorted list of notebook Paths under doc/tutorials. From 6783f1f71e8c2595a32c51a5328d614fec3d8d4a Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 10 Nov 2025 14:34:49 +0000 Subject: [PATCH 4/8] refactor: simplify kernel shutdown handling in notebook tests --- tests/test_tutorials.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_tutorials.py b/tests/test_tutorials.py index ea96dbf34..14bce8e5c 100644 --- a/tests/test_tutorials.py +++ b/tests/test_tutorials.py @@ -4,6 +4,7 @@ import pytest from nbclient import NotebookClient from nbclient.exceptions import CellExecutionError +import contextlib def _find_tutorial_notebooks(): @@ -50,7 +51,5 @@ def test_execute_notebook(nb_path): raise AssertionError(f"Error while executing notebook {nb_path}: {exc}") from exc finally: # Ensure kernel is shut down. - try: + with contextlib.suppress(Exception): client.shutdown_kernel() - except Exception: - pass From 782aa881108860e4c80fbd54e108410570ea847c Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 10 Nov 2025 14:35:16 +0000 Subject: [PATCH 5/8] organise imports again --- tests/test_tutorials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tutorials.py b/tests/test_tutorials.py index 14bce8e5c..91caead71 100644 --- a/tests/test_tutorials.py +++ b/tests/test_tutorials.py @@ -1,10 +1,10 @@ +import contextlib from pathlib import Path import nbformat import pytest from nbclient import NotebookClient from nbclient.exceptions import CellExecutionError -import contextlib def _find_tutorial_notebooks(): From 72c8dc03b9d6b6c1233fdaab7654de9ed3464900 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 10 Nov 2025 14:43:48 +0000 Subject: [PATCH 6/8] fix: update dependencies in CI workflow to include nbformat, nbclient, and ipykernel --- .github/workflows/python.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index e6611c86c..bad4bb0aa 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -29,7 +29,7 @@ jobs: - name: Set up dependencies run: | python -m pip install --upgrade pip - pip install setuptools build cython pytest pytest-skip-slow wheel lxml numpy scipy + pip install setuptools build cython pytest pytest-skip-slow wheel lxml numpy scipy nbformat nbclient ipykernel - name: Build source distribution run: python -m build @@ -56,7 +56,7 @@ jobs: - name: Set up dependencies run: | python -m pip install --upgrade pip - pip install cython pytest pytest-skip-slow wheel lxml numpy scipy + pip install cython pytest pytest-skip-slow wheel lxml numpy scipy nbformat nbclient ipykernel - name: Build extension run: | python -m pip install -v . @@ -79,7 +79,7 @@ jobs: - name: Set up dependencies run: | python -m pip install --upgrade pip - pip install cython pytest pytest-skip-slow wheel lxml numpy scipy + pip install cython pytest pytest-skip-slow wheel lxml numpy scipy nbformat nbclient ipykernel - name: Build extension run: | python -m pip install -v . @@ -102,7 +102,7 @@ jobs: - name: Set up dependencies run: | python -m pip install --upgrade pip - pip install cython pytest pytest-skip-slow wheel lxml numpy scipy + pip install cython pytest pytest-skip-slow wheel lxml numpy scipy nbformat nbclient ipykernel - name: Build extension run: | python -m pip install -v . From 74236d6f602729fe051fd30d23d76678bd056f5c Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 10 Nov 2025 14:47:36 +0000 Subject: [PATCH 7/8] style: fix indentation in docstrings for tutorial notebook tests --- tests/test_tutorials.py | 72 ++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/tests/test_tutorials.py b/tests/test_tutorials.py index 91caead71..efe0ab0f2 100644 --- a/tests/test_tutorials.py +++ b/tests/test_tutorials.py @@ -8,17 +8,17 @@ def _find_tutorial_notebooks(): - """Return a sorted list of notebook Paths under doc/tutorials. + """Return a sorted list of notebook Paths under doc/tutorials. - Skips the entire module if the tutorials directory does not exist. - """ - root = Path(__file__).resolve().parents[1] / "doc" / "tutorials" - if not root.exists(): - pytest.skip(f"Tutorials folder not found: {root}") - notebooks = sorted(root.rglob("*.ipynb")) - if not notebooks: - pytest.skip(f"No tutorial notebooks found in: {root}") - return notebooks + Skips the entire module if the tutorials directory does not exist. + """ + root = Path(__file__).resolve().parents[1] / "doc" / "tutorials" + if not root.exists(): + pytest.skip(f"Tutorials folder not found: {root}") + notebooks = sorted(root.rglob("*.ipynb")) + if not notebooks: + pytest.skip(f"No tutorial notebooks found in: {root}") + return notebooks # Discover notebooks at import time so pytest can parametrize them. @@ -27,29 +27,29 @@ def _find_tutorial_notebooks(): @pytest.mark.parametrize("nb_path", _NOTEBOOKS, ids=[p.name for p in _NOTEBOOKS]) def test_execute_notebook(nb_path): - """Execute a single Jupyter notebook and fail if any cell errors occur. - - This uses nbclient.NotebookClient to run the notebook in its parent directory - so relative paths within the notebook resolve correctly. - """ - nb = nbformat.read(str(nb_path), as_version=4) - - # Prefer the notebook's kernelspec if provided, otherwise let nbclient pick the default. - kernel_name = nb.metadata.get("kernelspec", {}).get("name") - - client = NotebookClient( - nb, - timeout=600, - kernel_name=kernel_name, - resources={"metadata": {"path": str(nb_path.parent)}}, - ) - - try: - client.execute() - except CellExecutionError as exc: - # Re-raise with more context so pytest shows which notebook failed. - raise AssertionError(f"Error while executing notebook {nb_path}: {exc}") from exc - finally: - # Ensure kernel is shut down. - with contextlib.suppress(Exception): - client.shutdown_kernel() + """Execute a single Jupyter notebook and fail if any cell errors occur. + + This uses nbclient.NotebookClient to run the notebook in its parent directory + so relative paths within the notebook resolve correctly. + """ + nb = nbformat.read(str(nb_path), as_version=4) + + # Prefer the notebook's kernelspec if provided, otherwise let nbclient pick the default. + kernel_name = nb.metadata.get("kernelspec", {}).get("name") + + client = NotebookClient( + nb, + timeout=600, + kernel_name=kernel_name, + resources={"metadata": {"path": str(nb_path.parent)}}, + ) + + try: + client.execute() + except CellExecutionError as exc: + # Re-raise with more context so pytest shows which notebook failed. + raise AssertionError(f"Error while executing notebook {nb_path}: {exc}") from exc + finally: + # Ensure kernel is shut down. + with contextlib.suppress(Exception): + client.shutdown_kernel() From 06ba18653cba683256658bc05d1fd3e2b320f0d2 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 10 Nov 2025 15:10:53 +0000 Subject: [PATCH 8/8] fix: skip notebook test on Python 3.9 in CI --- .github/workflows/python.yml | 8 +++++++- doc/tutorials/running_locally.rst | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index bad4bb0aa..b466c6a8a 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -38,7 +38,13 @@ jobs: cd dist pip install -v pygambit*.tar.gz - name: Run tests - run: pytest + run: | + if [ "${{ matrix.python-version }}" = "3.9" ]; then + # Python 3.9 on linux skips the notebook execution test (notebooks may require newer kernels/deps) + pytest -q -k 'not test_execute_notebook' + else + pytest + fi macos-13: runs-on: macos-13 diff --git a/doc/tutorials/running_locally.rst b/doc/tutorials/running_locally.rst index f17eae6e2..075511cec 100644 --- a/doc/tutorials/running_locally.rst +++ b/doc/tutorials/running_locally.rst @@ -4,7 +4,7 @@ How to run PyGambit tutorials on your computer ============================================== The PyGambit tutorials are available as Jupyter notebooks and can be run interactively using any program that supports Jupyter notebooks, such as JupyterLab or VSCode. -You will need a working installation of Python 3 (tested with 3.9 and later) on your machine. +You will need a working installation of Python 3.9+ on your machine to run PyGambit (however the tutorials contain some syntax that may not be compatible with earlier versions of Python than 3.13). 1. To download the tutorials, open your OS's command prompt and clone the Gambit repository from GitHub, then navigate to the tutorials directory: ::