Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
pip install -r dev_tools/requirements/envs/pytest.env.txt
pip install --no-deps -e .
- run: |
python dev_tools/execute-notebooks.py --n-workers=8
check/pytest-notebook
env:
NUMBA_NUM_THREADS: 4

Expand Down
20 changes: 20 additions & 0 deletions check/pytest-notebook
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash

################################################################################
# Runs notebook tests via jupytext-based execution.
################################################################################

# Get the working directory to the repo root.
thisdir="$(dirname "${BASH_SOURCE[0]}")" || exit $?
topdir="$(git -C "${thisdir}" rev-parse --show-toplevel)" || exit $?
cd "${topdir}" || exit $?

set -e

python dev_tools/check-notebook-tests.py

# Use non-interactive backend so plt.show() doesn't open GUI windows.
export MPLBACKEND=agg

pytest -v qualtran/ tutorials/ \
-m notebook
145 changes: 145 additions & 0 deletions dev_tools/check-notebook-tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Verify that every committed .ipynb has a corresponding pytest notebook test.

For each committed notebook, checks that:
1. A *_test.py file exists containing an execute_notebook('name') call
2. That test function is decorated with @pytest.mark.notebook

Usage:
python dev_tools/check-notebook-tests.py
"""

import ast
import subprocess
import sys
from pathlib import Path
from typing import Dict, Tuple

from qualtran_dev_tools.git_tools import get_git_root

_EXCLUDED_DIRS = {'dev_tools'}


def get_committed_notebooks(reporoot: Path) -> Dict[str, Path]:
"""Return {stem: relative_path} for all committed .ipynb files under reporoot.

Excludes notebooks in dev_tools/ since those are developer utilities,
not user-facing documentation.
"""
result = subprocess.run(
['git', 'ls-files', '*.ipynb'], capture_output=True, text=True, check=True, cwd=reporoot
Comment thread
mpharrigan marked this conversation as resolved.
Outdated
)
return {
Path(f).stem: Path(f)
for f in result.stdout.strip().split('\n')
if f and not any(Path(f).parts[0] == d for d in _EXCLUDED_DIRS)
}


def _is_notebook_marker(decorator: ast.expr) -> bool:
"""Check if a decorator is @pytest.mark.notebook."""
# Handle pytest.mark.notebook (attr chain)
if isinstance(decorator, ast.Attribute) and decorator.attr == 'notebook':
return True
return False


def find_notebook_tests(reporoot: Path) -> Dict[str, Tuple[Path, bool]]:
"""Find all execute_notebook() calls in test files.

Searches all *_test.py files under the repo root (including qualtran/
and tutorials/).

Returns {notebook_name: (test_file_path_relative, has_notebook_marker)}.
"""
results: Dict[str, Tuple[Path, bool]] = {}
for test_file in reporoot.rglob('*_test.py'):
try:
tree = ast.parse(test_file.read_text())
except SyntaxError:
continue

for node in ast.walk(tree):
if not isinstance(node, ast.FunctionDef):
continue
# Check if function body contains execute_notebook('xxx')
for child in ast.walk(node):
if (
isinstance(child, ast.Call)
and _is_execute_notebook_call(child)
and child.args
and isinstance(child.args[0], ast.Constant)
):
nb_name = child.args[0].value
# Check for @pytest.mark.notebook decorator
has_marker = any(_is_notebook_marker(dec) for dec in node.decorator_list)
results[nb_name] = (test_file.relative_to(reporoot), has_marker)
return results


def _is_execute_notebook_call(node: ast.Call) -> bool:
"""Check if a Call node is a call to execute_notebook (with or without module prefix)."""
if isinstance(node.func, ast.Attribute) and node.func.attr == 'execute_notebook':
return True
if isinstance(node.func, ast.Name) and node.func.id == 'execute_notebook':
return True
return False


def main():
reporoot = get_git_root()

committed = get_committed_notebooks(reporoot)
tested = find_notebook_tests(reporoot)

errors = []

for stem, nb_rel_path in sorted(committed.items()):
if stem not in tested:
# Suggest the likely test file location
nb_dir = nb_rel_path.parent
test_file = nb_dir / f'{stem}_test.py'
errors.append(
f" MISSING TEST: {nb_rel_path}\n"
f" Add to {test_file}:\n"
f"\n"
f" @pytest.mark.notebook\n"
f" def test_{stem}_notebook():\n"
f" qlt_testing.execute_notebook('{stem}')\n"
)
else:
test_file, has_marker = tested[stem]
if not has_marker:
errors.append(
f" MISSING MARKER: {nb_rel_path}\n"
f" The test in {test_file} calls execute_notebook('{stem}')\n"
f" but is not decorated with @pytest.mark.notebook.\n"
f" Add the decorator so this test runs in the notebooks CI job:\n"
f"\n"
f" @pytest.mark.notebook\n"
f" def test_...():\n"
)

if errors:
print(f"ERROR: {len(errors)} notebook(s) have issues:\n")
print("\n".join(errors))
sys.exit(1)

print(f"OK: All {len(committed)} notebooks have properly marked tests.")


if __name__ == '__main__':
main()
2 changes: 1 addition & 1 deletion dev_tools/conf/mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ follow_imports = silent
ignore_missing_imports = true

# Non-Google
[mypy-sympy.*,matplotlib.*,proto.*,pandas.*,scipy.*,freezegun.*,mpl_toolkits.*,networkx.*,ply.*,astroid.*,pytest.*,_pytest.*,pylint.*,setuptools.*,qiskit.*,quimb.*,pylatex.*,filelock.*,sortedcontainers.*,tqdm.*,plotly.*,dash.*,tensorflow_docs.*,fxpmath.*,ipywidgets.*,cachetools.*,pydot.*,nbformat.*,nbconvert.*,openfermion.*,pennylane.*,mpmath.*]
[mypy-sympy.*,matplotlib.*,proto.*,pandas.*,scipy.*,freezegun.*,mpl_toolkits.*,networkx.*,ply.*,astroid.*,pytest.*,_pytest.*,pylint.*,setuptools.*,qiskit.*,quimb.*,pylatex.*,filelock.*,sortedcontainers.*,tqdm.*,plotly.*,dash.*,tensorflow_docs.*,fxpmath.*,ipywidgets.*,cachetools.*,pydot.*,nbformat.*,nbconvert.*,openfermion.*,pennylane.*,mpmath.*,jupytext.*]
follow_imports = silent
ignore_missing_imports = true

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ test = [
"pytest-xdist",

# test executing notebooks
"jupytext",
"ipykernel",
"filelock",

Expand Down
22 changes: 22 additions & 0 deletions qualtran/Adjoint_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

from qualtran import testing as qlt_testing


@pytest.mark.notebook
def test_Adjoint_notebook():
qlt_testing.execute_notebook('Adjoint')
22 changes: 22 additions & 0 deletions qualtran/Autodoc_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

from qualtran import testing as qlt_testing


@pytest.mark.notebook
def test_Autodoc_notebook():
qlt_testing.execute_notebook('Autodoc')
22 changes: 22 additions & 0 deletions qualtran/Controlled_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

from qualtran import testing as qlt_testing


@pytest.mark.notebook
def test_Controlled_notebook():
qlt_testing.execute_notebook('Controlled')
22 changes: 22 additions & 0 deletions qualtran/DataTypes_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

from qualtran import testing as qlt_testing


@pytest.mark.notebook
def test_DataTypes_notebook():
qlt_testing.execute_notebook('DataTypes')
22 changes: 22 additions & 0 deletions qualtran/Protocols_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

from qualtran import testing as qlt_testing


@pytest.mark.notebook
def test_Protocols_notebook():
qlt_testing.execute_notebook('Protocols')
5 changes: 5 additions & 0 deletions qualtran/bloqs/arithmetic/bitwise_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,8 @@ def test_bitwisenot_classical_action(dtype, bitsize):
def test_bitwise_not_str():
bloq = BitwiseNot(QUInt(5))
assert str(bloq) == "BitwiseNot(5)"


@pytest.mark.notebook
def test_bitwise_notebook():
qlt_testing.execute_notebook('bitwise')
6 changes: 6 additions & 0 deletions qualtran/bloqs/arithmetic/controlled_add_or_subtract_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
Soquet,
SoquetT,
)
from qualtran import testing as qlt_testing
from qualtran.bloqs.arithmetic import Add, Negate, Subtract
from qualtran.bloqs.arithmetic.controlled_add_or_subtract import (
_ctrl_add_or_sub_signed,
Expand Down Expand Up @@ -130,3 +131,8 @@ def test_controlled_t_complexity():

_ = bloq.controlled().adjoint().t_complexity()
_ = bloq.adjoint().controlled().t_complexity()


@pytest.mark.notebook
def test_controlled_add_or_subtract_notebook():
qlt_testing.execute_notebook('controlled_add_or_subtract')
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pytest

from qualtran import BloqBuilder
from qualtran import testing as qlt_testing
from qualtran.bloqs.arithmetic.conversions.contiguous_index import (
_to_contg_index,
ToContiguousIndex,
Expand All @@ -31,3 +34,8 @@ def test_to_contiguous_index_t_complexity():
q0, q1, out = bb.add(ToContiguousIndex(bitsize, 2 * bitsize), mu=q0, nu=q1, s=out)
cbloq = bb.finalize(mu=q0, nu=q1, s=out)
assert cbloq.t_complexity().t == 4 * 29


@pytest.mark.notebook
def test_contiguous_index_notebook():
qlt_testing.execute_notebook('contiguous_index')
22 changes: 22 additions & 0 deletions qualtran/bloqs/arithmetic/conversions/conversions_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

from qualtran import testing as qlt_testing


@pytest.mark.notebook
def test_conversions_notebook():
qlt_testing.execute_notebook('conversions')
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

from qualtran import testing as qlt_testing


@pytest.mark.notebook
def test_error_analysis_for_fxp_arithmetic_notebook():
qlt_testing.execute_notebook('error_analysis_for_fxp_arithmetic')
Loading
Loading