Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
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
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
python-version: ["3.11", "3.12", "3.13"]
sphinx-version: ["6", "7", "8"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -34,16 +35,17 @@ jobs:
run : |
python -m pip install --upgrade pip
pip install -e.[testing]
pip install "sphinx>=${{ matrix.sphinx-version }},<${{ matrix.sphinx-version == '6' && '7' || matrix.sphinx-version == '7' && '8' || '9' }}"
- name: Run pytest
run: |
pytest --durations=10 --cov=sphinx_exercise --cov-report=xml --cov-report=term-missing
- name: Create cov
run: coverage xml
- name: Upload to Codecov
if: false && (matrix.python-version == '3.11')
if: false && (matrix.python-version == '3.11' && matrix.sphinx-version == '8')
uses: codecov/codecov-action@v4
with:
name: sphinx-exercise-pytest-py3.11
name: sphinx-exercise-pytest-py3.11-sphinx8
token: "${{ secrets.CODECOV_TOKEN }}"
flags: pytests
file: ./coverage.xml
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
# Changelog


## [v1.0.1](https://github.com/executablebooks/sphinx-exercise/tree/v1.0.1) (2024-07-01)

### Improved 👌

- Updates to testing infrastructure and minor bug fixes
- Fixed JupyterBuilder handling in test suite

## [v1.0.0](https://github.com/executablebooks/sphinx-exercise/tree/v1.0.0) (2024-01-15)

### Improved 👌

- Added support for Sphinx 7
- Updated build and CI infrastructure
- Fixed deprecation warnings for Sphinx 7 compatibility
- Improved type hints throughout codebase
- Fixed doctree node mutation issues by copying nodes
- Updated codecov to version 3
- Pinned matplotlib to 3.7.* for testing stability

## [v0.4.1](https://github.com/executablebooks/sphinx-exercise/tree/v0.4.1) (2023-1-23)

### Improved 👌
Expand Down
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ recursive-include sphinx_exercise *.css
recursive-include sphinx_exercise *.json
recursive-include sphinx_exercise *.mo
recursive-include sphinx_exercise *.po
recursive-include sphinx_exercise *.py
recursive-include sphinx_exercise *.py
19 changes: 9 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dynamic = ["version"]
description = "A Sphinx extension for producing exercises and solutions."
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.9"
requires-python = ">=3.11"
authors = [
{ name = "QuantEcon", email = "[email protected]" },
]
Expand All @@ -22,10 +22,9 @@ classifiers = [
"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",
"Programming Language :: Python :: 3.13",
"Topic :: Documentation",
"Topic :: Documentation :: Sphinx",
"Topic :: Software Development :: Documentation",
Expand All @@ -34,7 +33,7 @@ classifiers = [
]
dependencies = [
"sphinx-book-theme",
"sphinx>=5",
"sphinx>=6.1",
]

[project.optional-dependencies]
Expand All @@ -49,20 +48,20 @@ code_style = [
"pre-commit",
]
rtd = [
"myst-nb~=1.0.0",
"myst-nb>=1.1.0",
"sphinx-book-theme",
"sphinx_togglebutton",
"sphinx>=5,<8",
"sphinx>=6.1,<9",
]
testing = [
"beautifulsoup4",
"coverage",
"matplotlib==3.8.*",
"myst-nb~=1.0.0",
"matplotlib>=3.8",
"myst-nb>=1.1.0",
"pytest-cov",
"pytest-regressions",
"pytest~=8.0.0",
"sphinx>=5,<8",
"pytest>=8.0",
"sphinx>=6.1,<9",
"texsoup",
"defusedxml", # Required by sphinx-testing
]
Expand Down
1 change: 1 addition & 0 deletions sphinx_exercise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@

# Callback Functions


def purge_exercises(app: Sphinx, env: BuildEnvironment, docname: str) -> None:
"""Purge sphinx_exercise registry"""

Expand Down
24 changes: 13 additions & 11 deletions sphinx_exercise/directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,30 @@
:licences: see LICENSE for details
"""

import os
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The os import is added but only used for os.path.splitext(). Consider using pathlib.Path instead for consistency with modern Python practices, or document why os.path is preferred here.

Copilot uses AI. Check for mistakes.
from typing import List
from docutils.nodes import Node

from sphinx.util.docutils import SphinxDirective
from docutils import nodes
from docutils.nodes import Node
from docutils.parsers.rst import directives
from sphinx.locale import get_translation
from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective

from .nodes import (
exercise_node,
exercise_enumerable_node,
exercise_end_node,
exercise_enumerable_node,
exercise_node,
exercise_subtitle,
exercise_title,
solution_end_node,
solution_node,
solution_start_node,
solution_end_node,
exercise_title,
exercise_subtitle,
solution_title,
)
from docutils import nodes
from sphinx.util import logging

logger = logging.getLogger(__name__)

from sphinx.locale import get_translation
MESSAGE_CATALOG_NAME = "exercise"
translate = get_translation(MESSAGE_CATALOG_NAME)

Expand All @@ -40,7 +42,7 @@ def duplicate_labels(self, label):

if not label == "" and label in self.env.sphinx_exercise_registry.keys():
docpath = self.env.doc2path(self.env.docname)
path = docpath[: docpath.rfind(".")]
path = os.path.splitext(docpath)[0]
other_path = self.env.doc2path(
self.env.sphinx_exercise_registry[label]["docname"]
)
Expand Down
8 changes: 5 additions & 3 deletions sphinx_exercise/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@
from docutils import nodes as docutil_nodes
from sphinx import addnodes as sphinx_nodes
from sphinx.writers.latex import LaTeXTranslator
from sphinx.locale import get_translation
from .latex import LaTeXMarkup

logger = logging.getLogger(__name__)
LaTeX = LaTeXMarkup()


from sphinx.locale import get_translation
MESSAGE_CATALOG_NAME = "exercise"
translate = get_translation(MESSAGE_CATALOG_NAME)

Expand Down Expand Up @@ -54,7 +53,10 @@ class solution_end_node(docutil_nodes.Admonition, docutil_nodes.Element):
class exercise_title(docutil_nodes.title):
def default_title(self):
title_text = self.children[0].astext()
if title_text == f"{translate('Exercise')}" or title_text == f"{translate('Exercise')} %s":
if (
title_text == f"{translate('Exercise')}"
or title_text == f"{translate('Exercise')} %s"
):
return True
else:
return False
Expand Down
3 changes: 2 additions & 1 deletion sphinx_exercise/post_transforms.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import sphinx.addnodes as sphinx_nodes
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util import logging
Expand Down Expand Up @@ -195,7 +196,7 @@ def run(self):
except AttributeError:
docname = self.env.docname # for builder such as JupyterBuilder that don't support current_docname
docpath = self.env.doc2path(docname)
path = docpath[: docpath.rfind(".")]
path = os.path.splitext(docpath)[0]
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] While os.path.splitext() works correctly, the previous implementation using string slicing with rfind(".") would fail on paths without extensions. This change is an improvement, but consider using pathlib.Path(docpath).stem for a more robust solution.

Copilot uses AI. Check for mistakes.
msg = f"undefined label: {target_label}"
logger.warning(msg, location=path, color="red")
return
Expand Down
9 changes: 8 additions & 1 deletion sphinx_exercise/translations/_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

MESSAGE_CATALOG_NAME = "exercise"


def convert_json(folder=None):
folder = folder or Path(__file__).parent

Expand All @@ -19,7 +20,13 @@ def convert_json(folder=None):
english = data[0]["text"]
for item in data[1:]:
language = item["symbol"]
out_path = folder / "locales" / language / "LC_MESSAGES" / f"{MESSAGE_CATALOG_NAME}.po"
out_path = (
folder
/ "locales"
/ language
/ "LC_MESSAGES"
/ f"{MESSAGE_CATALOG_NAME}.po"
)
if not out_path.parent.exists():
out_path.parent.mkdir(parents=True)
if not out_path.exists():
Expand Down
4 changes: 2 additions & 2 deletions tests/books/test-gateddirective/solution-exercise-gated.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ 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)
cxy, f = axs[1].cohere(s1, s2, NFFT=256, Fs=1. / dt)
axs[1].set_ylabel('coherence')
fig.tight_layout()
Expand Down Expand Up @@ -87,7 +87,7 @@ 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)
cxy, f = axs[1].cohere(s1, s2, NFFT=256, Fs=1. / dt)
axs[1].set_ylabel('coherence')
fig.tight_layout()
Expand Down
4 changes: 2 additions & 2 deletions tests/books/test-gateddirective/solution-exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ 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)
cxy, f = axs[1].cohere(s1, s2, NFFT=256, Fs=1. / dt)
axs[1].set_ylabel('coherence')

fig.tight_layout()
Expand Down Expand Up @@ -85,7 +85,7 @@ 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)
cxy, f = axs[1].cohere(s1, s2, NFFT=256, Fs=1. / dt)
axs[1].set_ylabel('coherence')

fig.tight_layout()
Expand Down
7 changes: 6 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
def rootdir(tmpdir):
from sphinx.testing.path import path

src = path(__file__).parent.absolute() / "books"
# In Sphinx 6, path objects don't have .absolute() method, but they are already absolute
src = path(__file__).parent / "books"
dst = tmpdir.join("books")
shutil.copytree(src, dst)
yield path(dst)
Expand Down Expand Up @@ -94,6 +95,10 @@ class FileRegression:
(r"original_uri=\"[^\"]*\"\s", ""),
# TODO: Remove when support for Sphinx<7.2 is dropped
("Link to", "Permalink to"),
# Strip ipykernel process IDs (temporary directory paths)
(r"ipykernel_\d+", "ipykernel_XXXXX"),
# Normalize matplotlib image hashes (platform/version dependent)
(r"[a-f0-9]{64}\.png", "IMAGEHASH.png"),
)

def __init__(self, file_regression):
Expand Down
5 changes: 4 additions & 1 deletion tests/test_exercise_references.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from bs4 import BeautifulSoup
import pytest
import sphinx

SPHINX_VERSION = f".sphinx{sphinx.version_info[0]}"


@pytest.mark.sphinx("html", testroot="mybook")
Expand Down Expand Up @@ -35,7 +38,7 @@ def test_reference(app, idir, file_regression):
excs += f"{ref}\n"

file_regression.check(
str(excs[:-1]), basename=idir.split(".")[0], extension=".html"
str(excs[:-1]), basename=idir.split(".")[0], extension=f"{SPHINX_VERSION}.html"
)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<p>This is a reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">Exercise 1</span></a>.</p>
<p>This is a second reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some text 1</span></a>.</p>
<p>This is a third reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some text 1</span></a>.</p>
<p>This is a fourth reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some text Exercise</span></a>.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<p>This is a reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">Exercise 1</span></a>.</p>
<p>This is a second reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some text 1</span></a>.</p>
<p>This is a third reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some text 1</span></a>.</p>
<p>This is a fourth reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some text Exercise</span></a>.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<p>This is a reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">Exercise 1</span></a>.</p>
<p>This is a second reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some text 1</span></a>.</p>
<p>This is a third reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some text 1</span></a>.</p>
<p>This is a fourth reference <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some text Exercise</span></a>.</p>
<p class="topless"><a href="_enum_numref_title.html" title="previous chapter">_enum_numref_title</a></p>
<p class="topless"><a href="_unenum_mathtitle_label.html" title="next chapter">_unenum_mathtitle_label</a></p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<p>This is a reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">Exercise 2</span></a>.</p>
<p>This is a second reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">some text 2</span></a>.</p>
<p>This is a third reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">some text 2</span></a>.</p>
<p>This is a fourth reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">some text Exercise</span></a>.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<p>This is a reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">Exercise 2</span></a>.</p>
<p>This is a second reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">some text 2</span></a>.</p>
<p>This is a third reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">some text 2</span></a>.</p>
<p>This is a fourth reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">some text Exercise</span></a>.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<p>This is a reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">Exercise 2</span></a>.</p>
<p>This is a second reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">some text 2</span></a>.</p>
<p>This is a third reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">some text 2</span></a>.</p>
<p>This is a fourth reference <a class="reference internal" href="_enum_notitle_label.html#text-exc-notitle"><span class="std std-numref">some text Exercise</span></a>.</p>
<p class="topless"><a href="_enum_ref_mathtitle.html" title="previous chapter">_enum_ref_mathtitle</a></p>
<p class="topless"><a href="_enum_numref_title.html" title="next chapter">_enum_numref_title</a></p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<p>This reference does not include math <a class="reference internal" href="_enum_title_class_label.html#test-exc-label"><span class="std std-numref">some 3 text %s test Exercise</span></a>.</p>
<p>This reference does not include math <a class="reference internal" href="_enum_title_class_label.html#test-exc-label"><span class="std std-numref">some 3 text %s test</span></a>.</p>
<p>This reference does not include math <a class="reference internal" href="_enum_title_class_label.html#test-exc-label"><span class="std std-numref">some Exercise text %s test</span></a>.</p>
<p>This reference does not include math <a class="reference internal" href="_enum_title_class_label.html#test-exc-label"><span class="std std-numref">some Exercise text 3 test</span></a>.</p>
<p>This reference does not include math <a class="reference internal" href="_enum_title_class_label.html#test-exc-label"><span class="std std-numref">some 3 text test</span></a>.</p>
<p>This reference includes math <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some 1 text %s test Exercise</span></a>.</p>
<p>This reference includes math <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some 1 text %s test</span></a>.</p>
<p>This reference includes math <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some Exercise text %s test</span></a>.</p>
<p>This reference includes math <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some Exercise text 1 test</span></a>.</p>
<p>This reference includes math <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some 1 text test</span></a>.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<p>This reference does not include math <a class="reference internal" href="_enum_title_class_label.html#test-exc-label"><span class="std std-numref">some 3 text %s test Exercise</span></a>.</p>
<p>This reference does not include math <a class="reference internal" href="_enum_title_class_label.html#test-exc-label"><span class="std std-numref">some 3 text %s test</span></a>.</p>
<p>This reference does not include math <a class="reference internal" href="_enum_title_class_label.html#test-exc-label"><span class="std std-numref">some Exercise text %s test</span></a>.</p>
<p>This reference does not include math <a class="reference internal" href="_enum_title_class_label.html#test-exc-label"><span class="std std-numref">some Exercise text 3 test</span></a>.</p>
<p>This reference does not include math <a class="reference internal" href="_enum_title_class_label.html#test-exc-label"><span class="std std-numref">some 3 text test</span></a>.</p>
<p>This reference includes math <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some 1 text %s test Exercise</span></a>.</p>
<p>This reference includes math <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some 1 text %s test</span></a>.</p>
<p>This reference includes math <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some Exercise text %s test</span></a>.</p>
<p>This reference includes math <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some Exercise text 1 test</span></a>.</p>
<p>This reference includes math <a class="reference internal" href="_enum_mathtitle_label.html#test-exc-label-math"><span class="std std-numref">some 1 text test</span></a>.</p>
Loading