Skip to content

Commit 638a78f

Browse files
Tidy (#26)
1 parent dc1e908 commit 638a78f

23 files changed

+147
-81
lines changed

CHANGELOG.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,21 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.1] - 2025-02-19
9+
10+
### Changed
11+
12+
- Improved documentation
13+
- Explicitly show test helpers are an internal concern
14+
- Tidied up test assets
15+
816
## [0.2.0] - 2025-02-19
917

10-
## Added
18+
### Added
1119

1220
- Test execution reporting in session log and short summary report: [#21][pr-21] & [#24][pr-24]
1321

14-
## Changed
22+
### Changed
1523

1624
- `_parser.Notebook` API changed: added `.codecells` and `.testcells`, removed `get_codecells()`, `get_testcells()`, `.contents`
1725

README.md

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,60 @@
11
# pytest-ipynb2
22

3-
A pytest plugin to run tests in Jupyter Notebooks. Designed to integrate with [chmp/ipytest](https://github.com/chmp/ipytest).
3+
A pytest plugin to run tests in Jupyter Notebooks.
44

5-
For more details including installation, usage and known-issues: see the [docs](https://musicalninjadad.github.io/pytest-ipynb2)
5+
Designed to play nicely with [chmp/ipytest](https://github.com/chmp/ipytest).
6+
7+
## Why?
8+
9+
My use case is so that I can teach my son to code and use notebooks to do that but still have the tests show up in vscode test explorer.
10+
11+
We also like to have all our tests run in a github workflow - and this makes that simple too.
12+
13+
## Usage
14+
15+
Usage is very simple:
16+
17+
1. Install from pypi (e.g. with pip):
18+
19+
```sh
20+
pip install pytest-ipynb2
21+
```
22+
23+
1. Enable by adding to the default options in `pyproject.toml` ``:
24+
25+
```toml
26+
[tool.pytest.ini_options]
27+
addopts = [
28+
"-p pytest_ipynb2.plugin",
29+
]
30+
```
31+
32+
1. That's it! pytest will now collect and execute any tests in jupyter notebooks when run form the command line or IDE.
33+
34+
## Test identification
35+
36+
I'm assuming you also want to run your tests inside your notebooks ... so simply use the `%%ipytest` magic in a cell and pytest will collect any tests based on the usual naming and identification rules.
37+
38+
> **Note:** tests will *only* be identified in cells which use the `%%ipytest` magic
39+
40+
## Documentation
41+
42+
For more details see the [docs](https://musicalninjadad.github.io/pytest-ipynb2)
43+
44+
## Features
45+
46+
- Enables pytest to collect and execute tests stored within jupyter notebooks
47+
- Provides meaningful test logs identifying the notebook, cell and test function
48+
- Handles tests with fixtures and parametrization
49+
- Executes *all cells above* the test cell before running the tests in that cell.
50+
51+
>WARNING: this means that if any previous cells have side-effects they will occur on test collection, just as they would if included in a pytest test module.
52+
53+
## Known limitations & To-Dos
54+
55+
This is an early version. The following things are still on my to-do list:
56+
57+
- Handling tests grouped into classes [#22](https://github.com/MusicalNinjaDad/pytest-ipynb2/issues/22) (might work - I've not checked yet)
58+
- Assertion re-writing. Failed tests don't yet give the expected quality of pytest output about the failure [#20](https://github.com/MusicalNinjaDad/pytest-ipynb2/issues/20) (Workaround - re-run the test using ipytest inside the notebook)
59+
- v1.0.0 will include dedicated commandline options rather than requiring you to specify the plugin [#12](https://github.com/MusicalNinjaDad/pytest-ipynb2/issues/12)
60+
- This won't play nicely with other magics or direct calls to ipytest functions yet [#23](https://github.com/MusicalNinjaDad/pytest-ipynb2/issues/23)

__pyversion__

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.0
1+
0.2.1

docs/index.md

Lines changed: 0 additions & 3 deletions
This file was deleted.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../README.md

docs/parser.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# Parser (internal)
1+
# _parser
22

3-
The parser will likely be deprecated and merged into the plug-in classes in the future
3+
> **Warning**: The parser is currently an internal concern and changes will not necessarily be reflected in semver.
44
55
::: pytest_ipynb2._parser

docs/plugin.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
# Pytest plugin
1+
# Plugin - API
22

33
::: pytest_ipynb2.plugin

docs/test_helpers.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
# Test helpers
22

3-
::: pytest_ipynb2.pytester_helpers
3+
> **Warning**: These helpers are currently an internal concern and changes will not necessarily be reflected in semver.
4+
5+
::: pytest_ipynb2._pytester_helpers

mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ plugins:
4242
handlers:
4343
python:
4444
options:
45-
members: true
4645
show_bases: false
4746
show_root_heading: true
4847
heading_level: 2
@@ -52,6 +51,7 @@ plugins:
5251
signature_crossrefs: true
5352
separate_signature: true
5453
show_signature_annotations: true
54+
show_source: false
5555
docstring_section_style: spacy
5656
docstring_options:
5757
ignore_init_summary: true

pytest_ipynb2/_parser.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313

1414
class SourceList(list):
1515
"""
16-
A list with non-continuous indices for storing the contents of cells.
16+
A `list` with non-continuous indices for storing the contents of cells.
1717
18-
- use slicing: sourcelist[:], not list(sourcelist) to get contents.
19-
- supports .ids() analog to a mapping.keys(), yielding only cell-ids with source.
20-
- use .items() analog to a mapping, rather than enumerate().
18+
- use a full slice `sourcelist[:]`, not list(sourcelist) to get contents.
19+
- supports `.ids()` analog to a mapping.keys(), yielding only cell-ids with source.
2120
"""
2221

2322
def ids(self) -> Generator[int, None, None]:
23+
"""Analog to mapping `.keys()`, yielding only cell-ids with source."""
2424
for key, source in enumerate(self):
2525
if source is not None:
2626
yield key
@@ -32,6 +32,14 @@ def __getitem__(self, index: SupportsIndex) -> str: ...
3232
def __getitem__(self, index: slice) -> list[str]: ...
3333

3434
def __getitem__(self, index):
35+
"""
36+
Behaves as you would expect for a `list` with the following exceptions.
37+
38+
- If provided with a single `index`: Raises an IndexError if the element at `index` does not
39+
contain any relevant source.
40+
- If provided with a `slice`: Returns only those items, which contain relevant source.
41+
42+
"""
3543
underlying_list = list(self)
3644
if isinstance(index, slice):
3745
return [source for source in underlying_list[index] if source is not None]
@@ -44,17 +52,19 @@ def __getitem__(self, index):
4452

4553
class Notebook:
4654
"""
47-
An ipython Notebook.
55+
The relevant bits of an ipython Notebook.
4856
49-
- constructor from Path
50-
- methods to get various cell types
51-
- a `test` cell type identified by the `%%ipytest` cell magic.
57+
Attributes:
58+
codecells (SourceList): The code cells *excluding* any identified as test cells.
59+
testcells (SourceList): The code cells which are identified as containing tests, based
60+
upon the presence of the `%%ipytest`magic.
5261
"""
5362

5463
def __init__(self, filepath: Path) -> None:
5564
contents = nbformat.read(fp=str(filepath), as_version=4)
5665
nbformat.validate(contents)
5766
cells = contents.cells
67+
5868
for cell in cells:
5969
cell.source = [
6070
sourceline for sourceline in cell.source.splitlines() if not sourceline.startswith("ipytest")
@@ -65,9 +75,11 @@ def __init__(self, filepath: Path) -> None:
6575
else None
6676
for cell in cells
6777
)
78+
"""The code cells *excluding* any identified as test cells"""
6879
self.testcells = SourceList(
6980
"\n".join(line for line in cell.source if not line.startswith(r"%%ipytest")).strip()
7081
if cell.cell_type == "code" and any(line.startswith(r"%%ipytest") for line in cell.source)
7182
else None
7283
for cell in cells
7384
)
85+
"""The code cells which are identified as containing tests, based upon the presence of the `%%ipytest`magic."""

pytest_ipynb2/pytester_helpers.py renamed to pytest_ipynb2/_pytester_helpers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,8 @@ def example_dir(request: ExampleDirRequest, pytester: pytest.Pytester) -> Collec
230230
dir_node=dir_node,
231231
items=pytester.genitems([dir_node]),
232232
)
233+
234+
235+
def add_ipytest_magic(source: str) -> str:
236+
"""Add %%ipytest magic to the source code."""
237+
return f"%%ipytest\n\n{source}"

pytest_ipynb2/plugin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,11 @@ def _getobj(self) -> ModuleType:
7474
return dummy_module
7575

7676
def _reportinfo(self: pytest.Item) -> tuple[str, int, str | None]:
77-
"""Pytest checks whether `.obj.__code__.co_filename` matches `.path`."""
77+
"""Override pytest which checks `.obj.__code__.co_filename` == `.path`."""
7878
return self.path, 0, self.getmodpath()
7979

80-
def collect(self) -> Generator[pytest.Function | pytest.Class, None, None]:
81-
"""Hacky hack overriding of reportinfo."""
80+
def collect(self) -> Generator[pytest.Function, None, None]:
81+
"""Replace the reportinfo method on the children, if present."""
8282
for item in super().collect():
8383
if hasattr(item, "reportinfo"): # pragma: no branch # TODO(MusicalNinjaDad): #22 Tests grouped in Class
8484
item.reportinfo = self._reportinfo

tests/assets/globals_test.py

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
%%ipytest
2-
31
def test_fails():
42
x = 1
53
assert x == 2

tests/assets/fixture_test.py renamed to tests/assets/test_fixture.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
%%ipytest
2-
31
import pytest
42

53
@pytest.fixture

tests/assets/test_globals.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def test_globals():
2+
assert x == 1

tests/assets/param_test.py renamed to tests/assets/test_param.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
%%ipytest
2-
31
import pytest
42

53
@pytest.mark.parametrize(
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
%%ipytest
2-
31
def test_pass():
42
assert True

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
pytest_plugins = ["pytester", "pytest_ipynb2.pytester_helpers"]
3+
pytest_plugins = ["pytester", "pytest_ipynb2._pytester_helpers"]
44

55

66
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Function]) -> None: # noqa: ARG001

tests/test_collection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest_ipynb2
66
import pytest_ipynb2.plugin
7-
from pytest_ipynb2.pytester_helpers import CollectedDir, CollectionTree, ExampleDir
7+
from pytest_ipynb2._pytester_helpers import CollectedDir, CollectionTree, ExampleDir
88

99

1010
@pytest.fixture

tests/test_collectiontree.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77

88
import pytest
99

10-
from pytest_ipynb2.pytester_helpers import CollectionTree, ExampleDir
10+
from pytest_ipynb2._pytester_helpers import CollectionTree, ExampleDir
1111

1212
if TYPE_CHECKING:
13-
from pytest_ipynb2.pytester_helpers import CollectedDir
13+
from pytest_ipynb2._pytester_helpers import CollectedDir
1414

1515

1616
@pytest.fixture

tests/test_fixtures.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import nbformat
44
import pytest
55

6-
from pytest_ipynb2.pytester_helpers import CollectedDir, ExampleDir
6+
from pytest_ipynb2._pytester_helpers import CollectedDir, ExampleDir, add_ipytest_magic
77

88
tests = [
99
pytest.param(
@@ -32,17 +32,17 @@
3232
),
3333
pytest.param(
3434
ExampleDir(
35-
notebooks={"generated": [Path("tests/assets/passing_test.py").read_text()]},
35+
notebooks={"generated": [add_ipytest_magic(Path("tests/assets/test_passing.py").read_text())]},
3636
),
3737
{
3838
"generated.ipynb": [
39-
"\n".join( # noqa: FLY002
40-
[
41-
r"%%ipytest",
42-
"",
43-
"def test_pass():",
44-
" assert True",
45-
],
39+
add_ipytest_magic(
40+
"\n".join( # noqa: FLY002
41+
[
42+
"def test_pass():",
43+
" assert True",
44+
],
45+
),
4646
),
4747
],
4848
},
@@ -53,7 +53,7 @@
5353
notebooks={
5454
"generated": [
5555
Path("tests/assets/import_ipytest.py").read_text(),
56-
Path("tests/assets/passing_test.py").read_text(),
56+
add_ipytest_magic(Path("tests/assets/test_passing.py").read_text()),
5757
],
5858
},
5959
),
@@ -66,13 +66,13 @@
6666
"",
6767
],
6868
),
69-
"\n".join( # noqa: FLY002
70-
[
71-
r"%%ipytest",
72-
"",
73-
"def test_pass():",
74-
" assert True",
75-
],
69+
add_ipytest_magic(
70+
"\n".join( # noqa: FLY002
71+
[
72+
"def test_pass():",
73+
" assert True",
74+
],
75+
),
7676
),
7777
],
7878
},

tests/test_runtests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from pytest_ipynb2.pytester_helpers import CollectedDir, ExampleDir
6+
from pytest_ipynb2._pytester_helpers import CollectedDir, ExampleDir
77

88
if TYPE_CHECKING:
99
from pytest_ipynb2.plugin import Cell

0 commit comments

Comments
 (0)