From 26c7b27339f530c45ac557ebb0ec1d89c65cc365 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Wed, 6 Mar 2024 11:45:22 +0000 Subject: [PATCH] MAINT: update build and CI for Sphinx 7 (#65) * maint: update build and CI * maint: revert version to current * maint: more CI fixes,pre-commit * maint: fix quoting * maint: update pytest * maint: fix spec * maint: fix CI arg * wip: improving deprecations * maint: add type hints * fix: patch regression * maint: run pre-commit * fix: copy doctree node to avoid mutation * test: drop original_uri, translation_progress * test: fix tests * test: fix Sphinx 7 incompat * maint: run pre-commit --- .github/workflows/ci.yml | 44 ++--- .gitignore | 1 + .pre-commit-config.yaml | 13 +- MANIFEST.in | 11 -- pyproject.toml | 74 +++++++ setup.py | 81 -------- sphinx_exercise/__init__.py | 7 +- sphinx_exercise/_compat.py | 9 + sphinx_exercise/directive.py | 7 +- sphinx_exercise/latex.py | 1 - sphinx_exercise/post_transforms.py | 14 +- sphinx_exercise/transforms.py | 7 +- tests/conftest.py | 66 ++++++- tests/test_gateddirective.py | 12 +- .../solution-exercise-0.sphinx7.html | 43 +++++ .../solution-exercise-1.sphinx7.html | 43 +++++ .../solution-exercise-gated-0.sphinx7.html | 43 +++++ .../solution-exercise-gated-1.sphinx7.html | 43 +++++ .../solution-exercise-gated.sphinx7.xml | 111 +++++++++++ .../solution-exercise.sphinx7.xml | 114 +++++++++++ tests/test_latex/test_latex_build.sphinx7.tex | 181 ++++++++++++++++++ 21 files changed, 768 insertions(+), 157 deletions(-) delete mode 100644 MANIFEST.in create mode 100644 pyproject.toml delete mode 100644 setup.py create mode 100644 sphinx_exercise/_compat.py create mode 100644 tests/test_gateddirective/solution-exercise-0.sphinx7.html create mode 100644 tests/test_gateddirective/solution-exercise-1.sphinx7.html create mode 100644 tests/test_gateddirective/solution-exercise-gated-0.sphinx7.html create mode 100644 tests/test_gateddirective/solution-exercise-gated-1.sphinx7.html create mode 100644 tests/test_gateddirective/solution-exercise-gated.sphinx7.xml create mode 100644 tests/test_gateddirective/solution-exercise.sphinx7.xml create mode 100644 tests/test_latex/test_latex_build.sphinx7.tex diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 006ff23..23527b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,22 +12,22 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 with: - python-version: 3.9 - - uses: pre-commit/action@v2.0.0 + python-version: "3.11" + - uses: pre-commit/action@v3.0.1 tests: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -36,14 +36,14 @@ jobs: pip install -e.[testing] - name: Run pytest run: | - pytest --duration=10 --cov=sphinx_exercise --cov-report=xml --cov-report=term-missing + pytest --durations=10 --cov=sphinx_exercise --cov-report=xml --cov-report=term-missing - name: Create cov run: coverage xml - name: Upload to Codecov - if: matrix.python-version == 3.8 - uses: codecov/codecov-action@v1 + if: matrix.python-version == '3.11' + uses: codecov/codecov-action@v4 with: - name: sphinx-exercise-pytest-py3.8 + name: sphinx-exercise-pytest-py3.11 flags: pytests file: ./coverage.xml fail_ci_if_error: true @@ -52,11 +52,11 @@ jobs: name: Documentation build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip @@ -74,15 +74,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v1 + uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.11" - name: Build package run: | - pip install wheel - python setup.py bdist_wheel sdist + pip install wheel build + python -m build - name: Publish uses: pypa/gh-action-pypi-publish@v1.3.1 with: diff --git a/.gitignore b/.gitignore index 6db21ef..6fd03ab 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ dist/ coverage.* coverage.xml .tox/ +.venv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87ce973..e44f9e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,12 +22,9 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.4 hooks: - - id: flake8 - - - repo: https://github.com/psf/black - rev: 22.8.0 - hooks: - - id: black + - id: ruff + args: ["--fix", "--show-fixes"] + - id: ruff-format diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 7bb18ac..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,11 +0,0 @@ -exclude .flake8 -exclude .pre-commit-config.yaml -exclude .readthedocs.yml -exclude tox.ini - -include LICENSE -include MANIFEST.in -include README.md - -recursive-include sphinx_exercise *.js -recursive-include sphinx_exercise *.css diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..370b607 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,74 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "sphinx-exercise" +dynamic = ["version"] +description = "A Sphinx extension for producing exercises and solutions." +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.9" +authors = [ + { name = "QuantEcon", email = "admin@quantecon.org" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Plugins", + "Environment :: Web Environment", + "Framework :: Sphinx :: Extension", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Documentation", + "Topic :: Documentation :: Sphinx", + "Topic :: Software Development :: Documentation", + "Topic :: Text Processing", + "Topic :: Utilities", +] +dependencies = [ + "sphinx-book-theme", + "sphinx>=5", +] + +[project.optional-dependencies] +all = [ + "sphinx-exercise[code_style]", + "sphinx-exercise[rtd]", + "sphinx-exercise[testing]" +] +code_style = [ + "black", + "flake8<3.8.0,>=3.7.0", + "pre-commit", +] +rtd = [ + "myst-nb~=1.0.0", + "sphinx-book-theme", + "sphinx>=5,<8", +] +testing = [ + "beautifulsoup4", + "coverage", + "matplotlib", + "myst-nb~=1.0.0", + "pytest-cov", + "pytest-regressions", + "pytest~=8.0.0", + "sphinx>=5,<8", + "texsoup", +] + +[project.urls] +Homepage = "https://github.com/executablebooks/sphinx-exercise" +Source = "https://github.com/executablebooks/sphinx-exercise" +Tracker = "https://github.com/executablebooks/sphinx-exercise/issues" + +[tool.hatch.version] +path = "sphinx_exercise/__init__.py" diff --git a/setup.py b/setup.py deleted file mode 100644 index 7950697..0000000 --- a/setup.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- - -from setuptools import setup, find_packages - -VERSION = "v0.4.1" - -LONG_DESCRIPTION = """ -This package contains a [Sphinx](http://www.sphinx-doc.org/) extension -for producing exercise and solution directives. - -This project is maintained and supported by the Executable Books Project. -""" - -SHORT_DESCRIPTION = "A Sphinx extension for producing exercises and solutions." - -BASE_URL = "https://github.com/executablebooks/sphinx-exercise" -URL = f"{BASE_URL}/archive/{VERSION}.tar.gz" - -# Define all extras -extras = { - "code_style": ["flake8<3.8.0,>=3.7.0", "black", "pre-commit"], - "testing": [ - "coverage", - "pytest>=3.6,<4", - "pytest-cov", - "pytest-regressions", - "beautifulsoup4", - "myst-nb~=0.17.1", - "sphinx>=4,<6", - "docutils>=0.15,<0.19", - "texsoup", - "matplotlib", - ], - "rtd": [ - "sphinx>=4,<6", - "sphinx-book-theme", - "myst-nb~=0.17.1", - ], -} - -extras["all"] = set(ii for jj in extras.values() for ii in jj) - -setup( - name="sphinx-exercise", - version=VERSION, - python_requires=">=3.8", - author="QuantEcon", - author_email="admin@quantecon.org", - url=BASE_URL, - download_url=URL, - project_urls={ - "Source": BASE_URL, - "Tracker": f"{BASE_URL}/issues", - }, - description=SHORT_DESCRIPTION, - long_description=LONG_DESCRIPTION, - long_description_content_type="text/markdown", - license="BSD", - packages=find_packages(), - install_requires=["sphinx>=4", "sphinx-book-theme"], - extras_require=extras, - include_package_data=True, - classifiers=[ - "Development Status :: 4 - Beta", - "Environment :: Plugins", - "Environment :: Web Environment", - "Framework :: Sphinx :: Extension", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python", - "Topic :: Documentation :: Sphinx", - "Topic :: Documentation", - "Topic :: Software Development :: Documentation", - "Topic :: Text Processing", - "Topic :: Utilities", - ], -) diff --git a/sphinx_exercise/__init__.py b/sphinx_exercise/__init__.py index 217164a..d729f72 100644 --- a/sphinx_exercise/__init__.py +++ b/sphinx_exercise/__init__.py @@ -7,6 +7,9 @@ :license: MIT, see LICENSE for details. """ +__version__ = "0.4.1" + + from pathlib import Path from typing import Any, Dict, Set, Union, cast from sphinx.config import Config @@ -17,6 +20,7 @@ from sphinx.util import logging from sphinx.util.fileutil import copy_asset +from ._compat import findall from .directive import ( ExerciseDirective, ExerciseStartDirective, @@ -129,7 +133,7 @@ def doctree_read(app: Sphinx, document: Node) -> None: domain = cast(StandardDomain, app.env.get_domain("std")) # Traverse sphinx-exercise nodes - for node in document.traverse(): + for node in findall(document): if is_extension_node(node): name = node.get("names", [])[0] label = document.nameids[name] @@ -140,7 +144,6 @@ def doctree_read(app: Sphinx, document: Node) -> None: def setup(app: Sphinx) -> Dict[str, Any]: - app.add_config_value("hide_solutions", False, "env") app.connect("config-inited", init_numfig) # event order - 1 diff --git a/sphinx_exercise/_compat.py b/sphinx_exercise/_compat.py new file mode 100644 index 0000000..99e70b8 --- /dev/null +++ b/sphinx_exercise/_compat.py @@ -0,0 +1,9 @@ +from docutils.nodes import Element +from typing import Iterator + + +def findall(node: Element, *args, **kwargs) -> Iterator[Element]: + # findall replaces traverse in docutils v0.18 + # note a difference is that findall is an iterator + impl = getattr(node, "findall", node.traverse) + return iter(impl(*args, **kwargs)) diff --git a/sphinx_exercise/directive.py b/sphinx_exercise/directive.py index 0b0d36b..1a1f21d 100644 --- a/sphinx_exercise/directive.py +++ b/sphinx_exercise/directive.py @@ -88,7 +88,6 @@ class : str, } def run(self) -> List[Node]: - self.defaults = {"title_text": "Exercise"} self.serial_number = self.env.new_serialno() @@ -160,7 +159,10 @@ def run(self) -> List[Node]: self.env.sphinx_exercise_registry[label] = { "type": self.name, "docname": self.env.docname, - "node": node, + # Copy the node so that the post transforms do not modify this original state + # Prior to Sphinx 6.1.0, the doctree was not cached, and Sphinx loaded a new copy + # c.f. https://github.com/sphinx-doc/sphinx/commit/463a69664c2b7f51562eb9d15597987e6e6784cd + "node": node.deepcopy(), } # TODO: Could tag this as Hidden to prevent the cell showing @@ -214,7 +216,6 @@ class : str, solution_node = solution_node def run(self) -> List[Node]: - self.defaults = {"title_text": "Solution to"} target_label = self.arguments[0] self.serial_number = self.env.new_serialno() diff --git a/sphinx_exercise/latex.py b/sphinx_exercise/latex.py index dd24ee1..6eb04fd 100644 --- a/sphinx_exercise/latex.py +++ b/sphinx_exercise/latex.py @@ -2,7 +2,6 @@ class LaTeXMarkup(object): - CR = "\n" def visit_admonition(self): diff --git a/sphinx_exercise/post_transforms.py b/sphinx_exercise/post_transforms.py index 98e0666..32d49d2 100644 --- a/sphinx_exercise/post_transforms.py +++ b/sphinx_exercise/post_transforms.py @@ -4,6 +4,7 @@ from sphinx.builders.latex import LaTeXBuilder from docutils import nodes as docutil_nodes +from ._compat import findall from .utils import get_node_number, find_parent from .nodes import ( exercise_enumerable_node, @@ -46,11 +47,10 @@ class UpdateReferencesToEnumerated(SphinxPostTransform): default_priority = 5 def run(self): - if not hasattr(self.env, "sphinx_exercise_registry"): return - for node in self.document.traverse(sphinx_nodes.pending_xref): + for node in findall(self.document, sphinx_nodes.pending_xref): if node.get("reftype") != "numref": target_label = node.get("reftarget") if target_label in self.env.sphinx_exercise_registry: @@ -112,11 +112,10 @@ def resolve_title(self, node): return node def run(self): - if not hasattr(self.env, "sphinx_exercise_registry"): return - for node in self.document.traverse(is_exercise_node): + for node in findall(self.document, is_exercise_node): node = self.resolve_title(node) @@ -171,16 +170,14 @@ def resolve_solution_title(app, node, exercise_node): class ResolveTitlesInSolutions(SphinxPostTransform): - default_priority = 21 def run(self): - if not hasattr(self.env, "sphinx_exercise_registry"): return # Update Solution Directives - for node in self.document.traverse(solution_node): + for node in findall(self.document, solution_node): label = node.get("label") target_label = node.get("target_label") try: @@ -210,12 +207,11 @@ class ResolveLinkTextToSolutions(SphinxPostTransform): default_priority = 22 def run(self): - if not hasattr(self.env, "sphinx_exercise_registry"): return # Update Solution References - for node in self.document.traverse(docutil_nodes.reference): + for node in findall(self.document, docutil_nodes.reference): refid = node.get("refid") if refid in self.env.sphinx_exercise_registry: target = self.env.sphinx_exercise_registry[refid] diff --git a/sphinx_exercise/transforms.py b/sphinx_exercise/transforms.py index 72f8699..fdf2f48 100644 --- a/sphinx_exercise/transforms.py +++ b/sphinx_exercise/transforms.py @@ -7,6 +7,7 @@ # from sphinx.errors import ExtensionError +from ._compat import findall from .nodes import ( exercise_node, exercise_enumerable_node, @@ -88,7 +89,7 @@ def find_nodes(self, label, node): def apply(self): # Process all matching solution-start and solution-end nodes - for node in self.document.traverse(solution_start_node): + for node in findall(self.document, solution_start_node): label = node.get("label") parent_start, parent_end = self.find_nodes(label, node) if not parent_end: @@ -173,11 +174,11 @@ def merge_nodes(self, node): def apply(self): # Process all matching exercise and exercise-enumerable (gated=True) # and exercise-end nodes - for node in self.document.traverse(exercise_node): + for node in findall(self.document, exercise_node): if node.gated: self.merge_nodes(node) node.gated = False - for node in self.document.traverse(exercise_enumerable_node): + for node in findall(self.document, exercise_enumerable_node): if node.gated: self.merge_nodes(node) node.gated = False diff --git a/tests/conftest.py b/tests/conftest.py index e12ebf6..df54536 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,20 +1,35 @@ import shutil import pytest +import packaging.version +import sphinx +import re from pathlib import Path -from sphinx.testing.path import path pytest_plugins = "sphinx.testing.fixtures" -@pytest.fixture -def rootdir(tmpdir): - src = path(__file__).parent.abspath() / "books" - dst = tmpdir.join("books") - shutil.copytree(src, dst) - books = path(dst) - yield books - shutil.rmtree(dst) +if packaging.version.Version(sphinx.__version__) < packaging.version.Version("7.2.0"): + + @pytest.fixture + def rootdir(tmpdir): + from sphinx.testing.path import path + + src = path(__file__).parent.absolute() / "books" + dst = tmpdir.join("books") + shutil.copytree(src, dst) + yield path(dst) + shutil.rmtree(dst) + +else: + + @pytest.fixture + def rootdir(tmp_path): + src = Path(__file__).parent.absolute() / "books" + dst = tmp_path / "books" + shutil.copytree(src, dst) + yield dst + shutil.rmtree(dst) @pytest.fixture @@ -46,7 +61,8 @@ def read( extension = sphinx_version + extension # convert absolute filenames - for node in doctree.traverse(lambda n: "source" in n): + findall = getattr(doctree, "findall", doctree.traverse) + for node in findall(lambda n: "source" in n): node["source"] = Path(node["source"]).name if flatten_outdir: @@ -60,3 +76,33 @@ def read( return doctree return read + + +# comparison files will need updating +# alternatively the resolution of https://github.com/ESSS/pytest-regressions/issues/32 +@pytest.fixture() +def file_regression(file_regression): + return FileRegression(file_regression) + + +class FileRegression: + ignores = () + changes = ( + # TODO: Remove when support for Sphinx<=6 is dropped, + (re.escape(" translation_progress=\"{'total': 0, 'translated': 0}\""), ""), + # TODO: Remove when support for Sphinx<7.2 is dropped, + (r"original_uri=\"[^\"]*\"\s", ""), + # TODO: Remove when support for Sphinx<7.2 is dropped + ("Link to", "Permalink to"), + ) + + def __init__(self, file_regression): + self.file_regression = file_regression + + def check(self, data, **kwargs): + return self.file_regression.check(self._strip_ignores(data), **kwargs) + + def _strip_ignores(self, data): + for src, dst in self.changes: + data = re.sub(src, dst, data) + return data diff --git a/tests/test_gateddirective.py b/tests/test_gateddirective.py index dd1fe2a..4a493ca 100644 --- a/tests/test_gateddirective.py +++ b/tests/test_gateddirective.py @@ -4,7 +4,7 @@ import sphinx from bs4 import BeautifulSoup from sphinx.errors import ExtensionError - +from pathlib import Path from sphinx.testing.util import strip_escseq SPHINX_VERSION = f".sphinx{sphinx.version_info[0]}" @@ -14,7 +14,7 @@ @pytest.mark.parametrize("docname", ["exercise-gated.html"]) def test_gated_exercise_build(app, docname, file_regression): app.build() - path_to_html = app.outdir / docname + path_to_html = Path(app.outdir) / docname # get content markup soup = BeautifulSoup(path_to_html.read_text(encoding="utf8"), "html.parser") exercise_directives = soup.select("div.exercise") @@ -27,8 +27,7 @@ def test_gated_exercise_build(app, docname, file_regression): @pytest.mark.parametrize("docname", ["exercise-gated"]) def test_gated_exercise_doctree(app, docname, get_sphinx_app_doctree): # Clean Up Build Directory from Previous Runs - build_dir = "/".join(app.outdir.split("/")[:-1]) - shutil.rmtree(build_dir) + shutil.rmtree(str(app.outdir)) # Test app.build() get_sphinx_app_doctree( @@ -45,7 +44,7 @@ def test_gated_exercise_doctree(app, docname, get_sphinx_app_doctree): ) def test_gated_solution_build(app, docname, file_regression): app.build() - path_to_html = app.outdir / docname + path_to_html = Path(app.outdir) / docname # get content markup soup = BeautifulSoup(path_to_html.read_text(encoding="utf8"), "html.parser") solution_directives = soup.select("div.solution") @@ -60,8 +59,7 @@ def test_gated_solution_build(app, docname, file_regression): @pytest.mark.parametrize("docname", ["solution-exercise", "solution-exercise-gated"]) def test_gated_solution_doctree(app, docname, get_sphinx_app_doctree): # Clean Up Build Directory from Previous Runs - build_dir = "/".join(app.outdir.split("/")[:-1]) - shutil.rmtree(build_dir) + shutil.rmtree(str(app.outdir)) # Test app.build() get_sphinx_app_doctree( diff --git a/tests/test_gateddirective/solution-exercise-0.sphinx7.html b/tests/test_gateddirective/solution-exercise-0.sphinx7.html new file mode 100644 index 0000000..201fdd3 --- /dev/null +++ b/tests/test_gateddirective/solution-exercise-0.sphinx7.html @@ -0,0 +1,43 @@ +
+

Solution to Exercise 1

+
+

This is a solution to Non-Gated Exercise 1

+
+
+
import numpy as np
+import matplotlib.pyplot as plt
+
+# Fixing random state for reproducibility
+np.random.seed(19680801)
+
+dt = 0.01
+t = np.arange(0, 30, dt)
+nse1 = np.random.randn(len(t))                 # white noise 1
+nse2 = np.random.randn(len(t))                 # white noise 2
+
+# Two signals with a coherent part at 10Hz and a random part
+s1 = np.sin(2 * np.pi * 10 * t) + nse1
+s2 = np.sin(2 * np.pi * 10 * t) + nse2
+
+fig, axs = plt.subplots(2, 1)
+axs[0].plot(t, s1, t, s2)
+axs[0].set_xlim(0, 2)
+axs[0].set_xlabel('time')
+axs[0].set_ylabel('s1 and s2')
+axs[0].grid(True)
+
+cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt)
+axs[1].set_ylabel('coherence')
+
+fig.tight_layout()
+plt.show()
+
+
+
+
+_images/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png +
+
+

With some follow up text to the solution

+
+
diff --git a/tests/test_gateddirective/solution-exercise-1.sphinx7.html b/tests/test_gateddirective/solution-exercise-1.sphinx7.html new file mode 100644 index 0000000..83e4ad6 --- /dev/null +++ b/tests/test_gateddirective/solution-exercise-1.sphinx7.html @@ -0,0 +1,43 @@ +
+

Solution to Exercise 2 (Replicate Matplotlib Plot)

+
+

This is a solution to Non-Gated Exercise 1

+
+
+
import numpy as np
+import matplotlib.pyplot as plt
+
+# Fixing random state for reproducibility
+np.random.seed(19680801)
+
+dt = 0.01
+t = np.arange(0, 30, dt)
+nse1 = np.random.randn(len(t))                 # white noise 1
+nse2 = np.random.randn(len(t))                 # white noise 2
+
+# Two signals with a coherent part at 10Hz and a random part
+s1 = np.sin(2 * np.pi * 10 * t) + nse1
+s2 = np.sin(2 * np.pi * 10 * t) + nse2
+
+fig, axs = plt.subplots(2, 1)
+axs[0].plot(t, s1, t, s2)
+axs[0].set_xlim(0, 2)
+axs[0].set_xlabel('time')
+axs[0].set_ylabel('s1 and s2')
+axs[0].grid(True)
+
+cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt)
+axs[1].set_ylabel('coherence')
+
+fig.tight_layout()
+plt.show()
+
+
+
+
+_images/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png +
+
+

With some follow up text to the solution

+
+
diff --git a/tests/test_gateddirective/solution-exercise-gated-0.sphinx7.html b/tests/test_gateddirective/solution-exercise-gated-0.sphinx7.html new file mode 100644 index 0000000..01bba6b --- /dev/null +++ b/tests/test_gateddirective/solution-exercise-gated-0.sphinx7.html @@ -0,0 +1,43 @@ +
+

Solution to Exercise 3

+
+

This is a solution to Gated Exercise 1

+
+
+
import numpy as np
+import matplotlib.pyplot as plt
+
+# Fixing random state for reproducibility
+np.random.seed(19680801)
+
+dt = 0.01
+t = np.arange(0, 30, dt)
+nse1 = np.random.randn(len(t))                 # white noise 1
+nse2 = np.random.randn(len(t))                 # white noise 2
+
+# Two signals with a coherent part at 10Hz and a random part
+s1 = np.sin(2 * np.pi * 10 * t) + nse1
+s2 = np.sin(2 * np.pi * 10 * t) + nse2
+
+fig, axs = plt.subplots(2, 1)
+axs[0].plot(t, s1, t, s2)
+axs[0].set_xlim(0, 2)
+axs[0].set_xlabel('time')
+axs[0].set_ylabel('s1 and s2')
+axs[0].grid(True)
+
+cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt)
+axs[1].set_ylabel('coherence')
+
+fig.tight_layout()
+plt.show()
+
+
+
+
+_images/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png +
+
+

With some follow up text to the solution

+
+
diff --git a/tests/test_gateddirective/solution-exercise-gated-1.sphinx7.html b/tests/test_gateddirective/solution-exercise-gated-1.sphinx7.html new file mode 100644 index 0000000..5891ae0 --- /dev/null +++ b/tests/test_gateddirective/solution-exercise-gated-1.sphinx7.html @@ -0,0 +1,43 @@ +
+

Solution to Exercise 4 (Replicate Matplotlib Plot)

+
+

This is a solution to Gated Exercise 2

+
+
+
import numpy as np
+import matplotlib.pyplot as plt
+
+# Fixing random state for reproducibility
+np.random.seed(19680801)
+
+dt = 0.01
+t = np.arange(0, 30, dt)
+nse1 = np.random.randn(len(t))                 # white noise 1
+nse2 = np.random.randn(len(t))                 # white noise 2
+
+# Two signals with a coherent part at 10Hz and a random part
+s1 = np.sin(2 * np.pi * 10 * t) + nse1
+s2 = np.sin(2 * np.pi * 10 * t) + nse2
+
+fig, axs = plt.subplots(2, 1)
+axs[0].plot(t, s1, t, s2)
+axs[0].set_xlim(0, 2)
+axs[0].set_xlabel('time')
+axs[0].set_ylabel('s1 and s2')
+axs[0].grid(True)
+
+cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt)
+axs[1].set_ylabel('coherence')
+
+fig.tight_layout()
+plt.show()
+
+
+
+
+_images/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png +
+
+

With some follow up text to the solution

+
+
diff --git a/tests/test_gateddirective/solution-exercise-gated.sphinx7.xml b/tests/test_gateddirective/solution-exercise-gated.sphinx7.xml new file mode 100644 index 0000000..b4ee3df --- /dev/null +++ b/tests/test_gateddirective/solution-exercise-gated.sphinx7.xml @@ -0,0 +1,111 @@ + +
+ + Gated Solutions to exercise-gated.md + <paragraph> + A solution using the gated directive + <solution_node classes="solution" docname="solution-exercise-gated" hidden="False" ids="gated-exercise-solution-1" label="gated-exercise-solution-1" names="gated-exercise-solution-1" serial_number="0" target_label="gated-exercise-1" title="Solution to" type="solution"> + <solution_title> + Solution to + <section ids="solution-content"> + <paragraph> + This is a solution to Gated Exercise 1 + <container cell_index="1" cell_metadata="{}" classes="cell" exec_count="1" nb_element="cell_code"> + <container classes="cell_input" nb_element="cell_code_source"> + <literal_block language="ipython3" xml:space="preserve"> + import numpy as np + import matplotlib.pyplot as plt + + # Fixing random state for reproducibility + np.random.seed(19680801) + + dt = 0.01 + t = np.arange(0, 30, dt) + nse1 = np.random.randn(len(t)) # white noise 1 + nse2 = np.random.randn(len(t)) # white noise 2 + + # Two signals with a coherent part at 10Hz and a random part + s1 = np.sin(2 * np.pi * 10 * t) + nse1 + s2 = np.sin(2 * np.pi * 10 * t) + nse2 + + fig, axs = plt.subplots(2, 1) + axs[0].plot(t, s1, t, s2) + axs[0].set_xlim(0, 2) + axs[0].set_xlabel('time') + axs[0].set_ylabel('s1 and s2') + axs[0].grid(True) + + cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt) + axs[1].set_ylabel('coherence') + + fig.tight_layout() + plt.show() + <container classes="cell_output" nb_element="cell_code_output"> + <container nb_element="mime_bundle"> + <container mime_type="text/plain"> + <literal_block classes="output text_plain" language="myst-ansi" xml:space="preserve"> + <Figure size 640x480 with 2 Axes> + <container mime_type="image/png"> + <image candidates="{'*': '_build/jupyter_execute/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png'}" uri="_build/jupyter_execute/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png"> + <paragraph> + With some follow up text to the solution + <paragraph> + and then a solution to + <pending_xref refdoc="solution-exercise-gated" refdomain="std" refexplicit="False" reftarget="gated-exercise-2" reftype="ref" refwarn="True"> + <inline classes="xref std std-ref"> + gated-exercise-2 + <paragraph> + A solution using the gated directive + <solution_node classes="solution" docname="solution-exercise-gated" hidden="False" ids="gated-exercise-solution-2" label="gated-exercise-solution-2" names="gated-exercise-solution-2" serial_number="1" target_label="gated-exercise-2" title="Solution to" type="solution"> + <solution_title> + Solution to + <section ids="solution-content"> + <paragraph> + This is a solution to Gated Exercise 2 + <container cell_index="3" cell_metadata="{}" classes="cell" exec_count="2" nb_element="cell_code"> + <container classes="cell_input" nb_element="cell_code_source"> + <literal_block language="ipython3" xml:space="preserve"> + import numpy as np + import matplotlib.pyplot as plt + + # Fixing random state for reproducibility + np.random.seed(19680801) + + dt = 0.01 + t = np.arange(0, 30, dt) + nse1 = np.random.randn(len(t)) # white noise 1 + nse2 = np.random.randn(len(t)) # white noise 2 + + # Two signals with a coherent part at 10Hz and a random part + s1 = np.sin(2 * np.pi * 10 * t) + nse1 + s2 = np.sin(2 * np.pi * 10 * t) + nse2 + + fig, axs = plt.subplots(2, 1) + axs[0].plot(t, s1, t, s2) + axs[0].set_xlim(0, 2) + axs[0].set_xlabel('time') + axs[0].set_ylabel('s1 and s2') + axs[0].grid(True) + + cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt) + axs[1].set_ylabel('coherence') + + fig.tight_layout() + plt.show() + <container classes="cell_output" nb_element="cell_code_output"> + <container nb_element="mime_bundle"> + <container mime_type="text/plain"> + <literal_block classes="output text_plain" language="myst-ansi" xml:space="preserve"> + <Figure size 640x480 with 2 Axes> + <container mime_type="image/png"> + <image candidates="{'*': '_build/jupyter_execute/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png'}" uri="_build/jupyter_execute/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png"> + <paragraph> + With some follow up text to the solution + <section ids="references-to-solutions" names="references\ to\ solutions"> + <title> + References to Solutions + <paragraph> + This is a reference to + <pending_xref refdoc="solution-exercise-gated" refdomain="std" refexplicit="False" reftarget="gated-exercise-solution-1" reftype="ref" refwarn="True"> + <inline classes="xref std std-ref"> + gated-exercise-solution-1 diff --git a/tests/test_gateddirective/solution-exercise.sphinx7.xml b/tests/test_gateddirective/solution-exercise.sphinx7.xml new file mode 100644 index 0000000..f4a07b7 --- /dev/null +++ b/tests/test_gateddirective/solution-exercise.sphinx7.xml @@ -0,0 +1,114 @@ +<document source="solution-exercise.md"> + <section ids="gated-solutions-to-exercise-md" names="gated\ solutions\ to\ exercise.md"> + <title> + Gated Solutions to exercise.md + <paragraph> + A solution using the gated directive + <solution_node classes="solution" docname="solution-exercise" hidden="False" ids="solution-gated-1" label="solution-gated-1" names="solution-gated-1" serial_number="0" target_label="exercise-1" title="Solution to" type="solution"> + <solution_title> + Solution to + <section ids="solution-content"> + <paragraph> + This is a solution to Non-Gated Exercise 1 + <container cell_index="1" cell_metadata="{}" classes="cell" exec_count="1" nb_element="cell_code"> + <container classes="cell_input" nb_element="cell_code_source"> + <literal_block language="ipython3" xml:space="preserve"> + import numpy as np + import matplotlib.pyplot as plt + + # Fixing random state for reproducibility + np.random.seed(19680801) + + dt = 0.01 + t = np.arange(0, 30, dt) + nse1 = np.random.randn(len(t)) # white noise 1 + nse2 = np.random.randn(len(t)) # white noise 2 + + # Two signals with a coherent part at 10Hz and a random part + s1 = np.sin(2 * np.pi * 10 * t) + nse1 + s2 = np.sin(2 * np.pi * 10 * t) + nse2 + + fig, axs = plt.subplots(2, 1) + axs[0].plot(t, s1, t, s2) + axs[0].set_xlim(0, 2) + axs[0].set_xlabel('time') + axs[0].set_ylabel('s1 and s2') + axs[0].grid(True) + + cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt) + axs[1].set_ylabel('coherence') + + fig.tight_layout() + plt.show() + <container classes="cell_output" nb_element="cell_code_output"> + <container nb_element="mime_bundle"> + <container mime_type="text/plain"> + <literal_block classes="output text_plain" language="myst-ansi" xml:space="preserve"> + <Figure size 640x480 with 2 Axes> + <container mime_type="image/png"> + <image candidates="{'*': '_build/jupyter_execute/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png'}" uri="_build/jupyter_execute/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png"> + <paragraph> + With some follow up text to the solution + <paragraph> + and a solution to + <pending_xref refdoc="solution-exercise" refdomain="std" refexplicit="False" reftarget="exercise-2" reftype="ref" refwarn="True"> + <inline classes="xref std std-ref"> + exercise-2 + <solution_node classes="solution" docname="solution-exercise" hidden="False" ids="solution-gated-2" label="solution-gated-2" names="solution-gated-2" serial_number="1" target_label="exercise-2" title="Solution to" type="solution"> + <solution_title> + Solution to + <section ids="solution-content"> + <paragraph> + This is a solution to Non-Gated Exercise 1 + <container cell_index="3" cell_metadata="{}" classes="cell" exec_count="2" nb_element="cell_code"> + <container classes="cell_input" nb_element="cell_code_source"> + <literal_block language="ipython3" xml:space="preserve"> + import numpy as np + import matplotlib.pyplot as plt + + # Fixing random state for reproducibility + np.random.seed(19680801) + + dt = 0.01 + t = np.arange(0, 30, dt) + nse1 = np.random.randn(len(t)) # white noise 1 + nse2 = np.random.randn(len(t)) # white noise 2 + + # Two signals with a coherent part at 10Hz and a random part + s1 = np.sin(2 * np.pi * 10 * t) + nse1 + s2 = np.sin(2 * np.pi * 10 * t) + nse2 + + fig, axs = plt.subplots(2, 1) + axs[0].plot(t, s1, t, s2) + axs[0].set_xlim(0, 2) + axs[0].set_xlabel('time') + axs[0].set_ylabel('s1 and s2') + axs[0].grid(True) + + cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt) + axs[1].set_ylabel('coherence') + + fig.tight_layout() + plt.show() + <container classes="cell_output" nb_element="cell_code_output"> + <container nb_element="mime_bundle"> + <container mime_type="text/plain"> + <literal_block classes="output text_plain" language="myst-ansi" xml:space="preserve"> + <Figure size 640x480 with 2 Axes> + <container mime_type="image/png"> + <image candidates="{'*': '_build/jupyter_execute/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png'}" uri="_build/jupyter_execute/5c3763e4602ed5d37252a09ed1d3b109e1e9c5ee20255a5d4c0d436f43743816.png"> + <paragraph> + With some follow up text to the solution + <section ids="references" names="references"> + <title> + References + <paragraph> + This is a reference to + <pending_xref refdoc="solution-exercise" refdomain="std" refexplicit="False" reftarget="solution-gated-1" reftype="ref" refwarn="True"> + <inline classes="xref std std-ref"> + solution-gated-1 + <paragraph> + This is a reference to + <pending_xref refdoc="solution-exercise" refdomain="std" refexplicit="False" reftarget="solution-gated-2" reftype="ref" refwarn="True"> + <inline classes="xref std std-ref"> + solution-gated-2 diff --git a/tests/test_latex/test_latex_build.sphinx7.tex b/tests/test_latex/test_latex_build.sphinx7.tex new file mode 100644 index 0000000..e9f1510 --- /dev/null +++ b/tests/test_latex/test_latex_build.sphinx7.tex @@ -0,0 +1,181 @@ +\begin{document} + +\ifdefined\shorthandoff + \ifnum\catcode`\=\string=\active\shorthandoff{=}\fi + \ifnum\catcode`\"=\active\shorthandoff{"}\fi +\fi + +\pagestyle{empty} +\sphinxmaketitle +\pagestyle{plain} +\sphinxtableofcontents +\pagestyle{normal} +\phantomsection\label{\detokenize{index::doc}} + + +\sphinxstepscope + + +\chapter{Exercise} +\label{\detokenize{exercise:exercise}}\label{\detokenize{exercise::doc}} +\sphinxAtStartPar +A collection of exercise directives +\phantomsection \label{exercise:exercise-1} + +\begin{sphinxadmonition}{note}{Exercise 1 (\protect\(n!\protect\) factorial)} + + + +\sphinxAtStartPar +Exercise 1 about \(n!\) factorial +\end{sphinxadmonition} +\phantomsection \label{exercise:exercise-2} +\begin{sphinxadmonition}{note}{Exercise (\protect\(n!\protect\) factorial)} + + + +\sphinxAtStartPar +Exercise 2 about \(n!\) factorial +\end{sphinxadmonition} +\phantomsection \label{exercise:exercise-3} + +\begin{sphinxadmonition}{note}{Exercise 2} + + + +\sphinxAtStartPar +Exercise 3 Content with Number +\end{sphinxadmonition} +\phantomsection \label{exercise:exercise-4} +\begin{sphinxadmonition}{note}{Exercise} + + + +\sphinxAtStartPar +Exercise 4 Content with no Number +\end{sphinxadmonition} + + +\section{References} +\label{\detokenize{exercise:references}} + +\subsection{Standard References} +\label{\detokenize{exercise:standard-references}} +\sphinxAtStartPar +This is a link to \DUrole{xref,std,std-ref}{exercise\sphinxhyphen{}no\sphinxhyphen{}title} + +\sphinxAtStartPar +This is a link to \hyperref[exercise:exercise-1]{Exercise 1} + +\sphinxAtStartPar +This is a link to {\hyperref[\detokenize{exercise:exercise-2}]{\sphinxcrossref{\DUrole{std,std-ref}{Exercise}}}} + +\sphinxAtStartPar +This ia another link with custom text \hyperref[exercise:exercise-3]{Exercise 3 Custom Text} + +\sphinxAtStartPar +This ia another link with custom text {\hyperref[\detokenize{exercise:exercise-4}]{\sphinxcrossref{\DUrole{std,std-ref}{Exercise 4 Custom Text}}}} + + +\subsection{Numbered References} +\label{\detokenize{exercise:numbered-references}} +\sphinxAtStartPar +This is a numbered reference to \hyperref[exercise:exercise-1]{Exercise 1} + +\sphinxAtStartPar +This is a numbered reference to \sphinxcode{\sphinxupquote{exercise\sphinxhyphen{}2}} and should be broken as exercise 2 is not an +enumerated exercise node. + +\sphinxAtStartPar +This is a numbered reference to \hyperref[exercise:exercise-3]{Exercise 2} + +\sphinxAtStartPar +This is a numbered reference with custom text to \hyperref[exercise:exercise-3]{Custom Text with a Number 2} + +\sphinxAtStartPar +This is a numbered reference to \sphinxcode{\sphinxupquote{exercise\sphinxhyphen{}4}} and should be broken as exercise 2 is not an +enumerated exercise node. + +\sphinxAtStartPar +This is a numbered reference with custom text to \sphinxcode{\sphinxupquote{Custom Text with a Number \{number\}}} and should be broken as exercise 2 is not an +enumerated exercise node. + +\sphinxstepscope + + +\chapter{Solution} +\label{\detokenize{solution:solution}}\label{\detokenize{solution::doc}} +\sphinxAtStartPar +A collection of solution directives +\phantomsection \label{solution:solution-1} + +\begin{sphinxadmonition}{note}{Solution to Exercise 1 (\protect\(n!\protect\) factorial)} + + + +\sphinxAtStartPar +This is a solution to exercise 1 +\end{sphinxadmonition} +\phantomsection \label{solution:solution-2} + +\begin{sphinxadmonition}{note}{Solution to Exercise (\protect\(n!\protect\) factorial)} + + + +\sphinxAtStartPar +This is a solution to exercise 2 +\end{sphinxadmonition} +\phantomsection \label{solution:solution-3} + +\begin{sphinxadmonition}{note}{Solution to Exercise 2} + + + +\sphinxAtStartPar +This is a solution to exercise 3 +\end{sphinxadmonition} +\phantomsection \label{solution:solution-4} + +\begin{sphinxadmonition}{note}{Solution to Exercise} + + + +\sphinxAtStartPar +This is a solution to exercise 4 +\end{sphinxadmonition} + + +\section{References} +\label{\detokenize{solution:references}} + +\subsection{Standard References} +\label{\detokenize{solution:standard-references}} +\sphinxAtStartPar +This is a link to {\hyperref[\detokenize{solution:solution-1}]{\sphinxcrossref{\DUrole{std,std-ref}{Solution to Exercise 1 (n! factorial)}}}} + +\sphinxAtStartPar +This is a link to {\hyperref[\detokenize{solution:solution-2}]{\sphinxcrossref{\DUrole{std,std-ref}{Solution to Exercise (n! factorial)}}}} + +\sphinxAtStartPar +This is a link to {\hyperref[\detokenize{solution:solution-3}]{\sphinxcrossref{\DUrole{std,std-ref}{Solution to Exercise 2}}}} + +\sphinxAtStartPar +This is a link to {\hyperref[\detokenize{solution:solution-4}]{\sphinxcrossref{\DUrole{std,std-ref}{Solution to Exercise}}}} + +\sphinxAtStartPar +This ia another link to a different {\hyperref[\detokenize{solution:solution-1}]{\sphinxcrossref{\DUrole{std,std-ref}{Solution to Exercise 1 (n! factorial)}}}} + + +\subsection{Numbered References} +\label{\detokenize{solution:numbered-references}} +\sphinxAtStartPar +Solution nodes are not enumerated nodes so these won’t work + +\sphinxAtStartPar +This is a link to \sphinxcode{\sphinxupquote{solution\sphinxhyphen{}1}} + + + +\renewcommand{\indexname}{Index} +\printindex +\end{document}