Skip to content

Commit 07171e5

Browse files
authored
Merge pull request #45 from executablebooks/gated-directive
ENH: Add gated directives to support code-cells for exercises and solutions
2 parents 4097d37 + a16787b commit 07171e5

33 files changed

+1731
-7
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ exclude: >
1010
tests/test_solution/.*|
1111
tests/test_solution_references/.*|
1212
tests/test_hiddendirective/.*|
13+
tests/test_gateddirective/.*|
1314
)$
1415
1516
repos:
376 KB
Loading

docs/source/syntax.md

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,112 @@ factorial(4)
151151
_Source:_ [QuantEcon](https://python-programming.quantecon.org/functions.html#Exercise-1)
152152

153153

154+
## Alternative Gated Syntax
155+
156+
A restriction of MyST is that `code-cell` directives must be at the root level of the document for them to be executed. This maintains direct
157+
compatility with the `jupyter notebook` and enables tools like `jupytext` to convert between `myst` and `ipynb` files.
158+
159+
As a result **executable** `code-cell` directives cannot be nested inside of exercises or solution directives.
160+
161+
The solution to this is to use the **gated syntax**.
162+
163+
```{note}
164+
This syntax can also be a convenient way of surrounding blocks of text that may include other directives that you wish
165+
to include in an exercise or solution admonition.
166+
```
167+
168+
### Basic Syntax
169+
170+
````md
171+
```{exercise-start}
172+
:label: ex1
173+
```
174+
175+
```{code-cell}
176+
# Some setup code that needs executing
177+
```
178+
179+
and maybe you wish to add a figure
180+
181+
```{figure} img/example.png
182+
```
183+
184+
```{exercise-end}
185+
```
186+
````
187+
188+
The `exercise-start` directive allows for he same options as core `exercise` directive.
189+
190+
````md
191+
```{solution-start} ex1
192+
```
193+
194+
```{code-cell}
195+
# Solution Code
196+
```
197+
198+
```{solution-end}
199+
```
200+
````
201+
202+
```{warning}
203+
If there are missing `-start` and `-end` directives, this will cause Sphinx to return an extension error,
204+
alongside some helpful feedback to diagnose the issue in document structure.
205+
```
206+
207+
### Example
208+
209+
````md
210+
211+
```{solution-start} exercise-1
212+
:label: solution-gated-1
213+
```
214+
215+
This is a solution to Exercise 1
216+
217+
```{code-cell} python3
218+
import numpy as np
219+
import matplotlib.pyplot as plt
220+
221+
# Fixing random state for reproducibility
222+
np.random.seed(19680801)
223+
224+
dt = 0.01
225+
t = np.arange(0, 30, dt)
226+
nse1 = np.random.randn(len(t)) # white noise 1
227+
nse2 = np.random.randn(len(t)) # white noise 2
228+
229+
# Two signals with a coherent part at 10Hz and a random part
230+
s1 = np.sin(2 * np.pi * 10 * t) + nse1
231+
s2 = np.sin(2 * np.pi * 10 * t) + nse2
232+
233+
fig, axs = plt.subplots(2, 1)
234+
axs[0].plot(t, s1, t, s2)
235+
axs[0].set_xlim(0, 2)
236+
axs[0].set_xlabel('time')
237+
axs[0].set_ylabel('s1 and s2')
238+
axs[0].grid(True)
239+
240+
cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt)
241+
axs[1].set_ylabel('coherence')
242+
243+
fig.tight_layout()
244+
plt.show()
245+
```
246+
247+
With some follow up text to the solution
248+
249+
```{solution-end}
250+
```
251+
252+
````
253+
254+
will produce the following `solution` block in your `html` output.
255+
256+
```{figure} img/gated-directive-example.png
257+
```
258+
259+
154260
### Referencing Solutions
155261

156262
You can refer to a solution using the `{ref}` role like: ```{ref}`my-solution` ``` the output of which depends on the attributes of the linked directive. If the linked directive is enumerable, the role will replace the solution reference with the linked directive type and its appropriate number like so: {ref}`my-solution`.
@@ -277,16 +383,16 @@ Any specific directive can be hidden by introducing the `:hidden:` option. For e
277383

278384
````md
279385
```{exercise}
280-
:hidden:
386+
:hidden:
281387

282-
This is a hidden exercise directive.
388+
This is a hidden exercise directive.
283389
```
284390
````
285391

286392
```{exercise}
287-
:hidden:
393+
:hidden:
288394
289-
This is a hidden exercise directive.
395+
This is a hidden exercise directive.
290396
```
291397

292398
### Remove All Solutions

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"beautifulsoup4",
2828
"myst-nb",
2929
"texsoup",
30+
"matplotlib",
3031
],
3132
"rtd": [
3233
"sphinx>=3.0",

sphinx_exercise/__init__.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,27 @@
1717
from sphinx.util import logging
1818
from sphinx.util.fileutil import copy_asset
1919

20-
from .directive import ExerciseDirective, SolutionDirective
20+
from .directive import (
21+
ExerciseDirective,
22+
ExerciseStartDirective,
23+
ExerciseEndDirective,
24+
SolutionDirective,
25+
SolutionStartDirective,
26+
SolutionEndDirective,
27+
)
2128
from .nodes import (
2229
exercise_node,
2330
visit_exercise_node,
2431
depart_exercise_node,
2532
exercise_enumerable_node,
2633
visit_exercise_enumerable_node,
2734
depart_exercise_enumerable_node,
35+
exercise_end_node,
2836
solution_node,
2937
visit_solution_node,
3038
depart_solution_node,
39+
solution_start_node,
40+
solution_end_node,
3141
is_extension_node,
3242
exercise_title,
3343
exercise_subtitle,
@@ -37,6 +47,11 @@
3747
visit_exercise_latex_number_reference,
3848
depart_exercise_latex_number_reference,
3949
)
50+
from .transforms import (
51+
CheckGatedDirectives,
52+
MergeGatedSolutions,
53+
MergeGatedExercises,
54+
)
4055
from .post_transforms import (
4156
ResolveTitlesInExercises,
4257
ResolveTitlesInSolutions,
@@ -159,6 +174,9 @@ def setup(app: Sphinx) -> Dict[str, Any]:
159174

160175
# Internal Title Nodes that don't need visit_ and depart_ methods
161176
# as they are resolved in post_transforms to docutil and sphinx nodes
177+
app.add_node(exercise_end_node)
178+
app.add_node(solution_start_node)
179+
app.add_node(solution_end_node)
162180
app.add_node(exercise_title)
163181
app.add_node(exercise_subtitle)
164182
app.add_node(solution_title)
@@ -173,7 +191,15 @@ def setup(app: Sphinx) -> Dict[str, Any]:
173191
)
174192

175193
app.add_directive("exercise", ExerciseDirective)
194+
app.add_directive("exercise-start", ExerciseStartDirective)
195+
app.add_directive("exercise-end", ExerciseEndDirective)
176196
app.add_directive("solution", SolutionDirective)
197+
app.add_directive("solution-start", SolutionStartDirective)
198+
app.add_directive("solution-end", SolutionEndDirective)
199+
200+
app.add_transform(CheckGatedDirectives)
201+
app.add_transform(MergeGatedExercises)
202+
app.add_transform(MergeGatedSolutions)
177203

178204
app.add_post_transform(UpdateReferencesToEnumerated)
179205
app.add_post_transform(ResolveTitlesInExercises)

0 commit comments

Comments
 (0)