You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Embed executable code inside of exercise solutions (Prototyping) #24
In #18@jstac brought up a good point that it is quite common to want executable code in the solutions to your exercises. Right now this isn't possible with {code-cell} because this breaks the flat cell structure that Jupyter Notebooks enforce.
This is an issue to brainstorm how to get this working!
Some options
Use start/stop directives for solutions
As @chrisjsewell suggests, we could provide another directive here that would mark the start/stop of a solution. Something like:
```{solution-start}
```
Intro to solution
```{code-cell}
print(2+2)
```
```{solution-stop}
```
Improve cell copy/pasting functionality to make this easier
If we improved the cell copy/pasting functionality we could do something like:
% This code cell would be executed
```{code-cell}:tags: remove-cell:name: solution1print(2+2)```
% And here it'd be placed inside the solution block
````{solution}```{ipynb:cell} solution1```````
Recommend jupyter-sphinx for solutions/exercises
As long as the code in the solution doesn't require the state of the kernel that came from running any previous code, then jupyter-sphinx should work just fine here. What won't work is if we define variables earlier in the page and then want to reference those variables in the solution.
One related alternative would be to replace solution-start with solution-header and drop solution-stop.
Then the only think that's being inserted is a heading that tells us what exercise is being solved, while the contents of the solution is just ordinary notebook content.
I suppose the exercise could be symmetric, with exercise-header inserting "Exercise x", typeset nicely as determined by the extension, and the content of the exercise again being ordinary notebook content underneath the header.
Just to double check, it wouldn't be possible to automate the gluing functionality so that whenever a code-cell is introduced in the content of the directive, it is replaced with a code block and the output of the code-cell - which can be added at the bottom of the document with a remove-cell tag - is glued in the directive?
The only issue I have with @jstac's solution-header suggestion is that we wouldn't be able to duplicate/copy solutions to another location as suggested in issue #11.
One related alternative would be to replace solution-start with solution-header and drop solution-stop.
One downside here would be if you wanted to apply styling to the solution text (i.e. in similar fashion to an admonition). I think it would be useful to have a end demarcation so that the solution block is identified in the ast.
This is actually similar to the root issue behind executablebooks/MyST-NB#274. The motivation behind this request is to be able to bring in code from a file (as used to be done with literalinclude) but jupytext (rightly) has no knowledge of anything outside of code-cell breaks.
I am in favour of option1 style approach to this -- mainly because the content stays ordered. If we introduce jupytext filters as a way to update content such as links to a website (instead of to other notebooks) to support download notebooks for websites then having notebooks with content in the correct order will make this work easier.
Could we have an argument start and end -- rather than two different directives:
```{solution} start
```
<Content of Solution>
```{solution} end
```
I guess we should also think about other cases where nested code-cell may not work as we will want to adopt a consistent approach to solving this issue. The only other that comes to mind is perhaps tabbed blocks -- but that syntax would get pretty messy if we were to add something like this to tabbed
As a user, I would be happy enough with the original option 1, although I do see your point about other nested code cells and the need for a consistent approach. Option 1 doesn't feel consistent with the rest of myst.
hey @jstac this is a nice format option -- I like it. I think we will still get issues with executing the nested code-blocks (cc: @chrisjsewell) as it breaks the linear document jupyter notebook rule for the code-blocks though.
I wonder if we could have tabbed code blocks in jupyter notebooks which had an execution rule of left to right. This would then directly translate to the notebooks format but this would essentially require a jupyter extension I think to change the base jupyter notebook format.
Just a note as well that we can always use jupyter-sphinx to execute and embed outputs in the built docs, and we can embed jupyter-sphinx execution inside of admonitions, tabs, etc. So you could have a MyST-NB notebook that also has jupyter-sphinx directives in it, and the {code-cell} blocks would just be executed separately from the jupyter-sphinx execution. The only challenge is that jupyter-sphinx won't have access to the same kernel that jupyter-cache uses when it executes the notebook. But this might still work for certain kinds of exercises.
thanks @choldgraf -- that is a thought I had as well and might be the best we can do (unless jupyter-cache implements something like pre-execution filters/transforms). My concern though is the document will necessarily need more imports and it "looks" like it would flow through the document linearly -- when in fact execution is not. It also complicates the production of support download notebooks.
In sphinxcontrib-jupyter the code-block (nestedness) was ignored and a code-cell block would just be written following the exercise but we can't do that here as the action takes place in jupyter-cache first.
What do you think about a jupyter-cache transform that could look for any nested code-cells and promote them to be code-cells where they occur:
If the the {code-cell} occurs in a markdown block it would (a) split the markdown block, (b) copy the contents of code-cell to a code-cell in the notebook json, (b) remove the {code-cell} from the text.
Given there is no real translation just a simple parse task -- this may have some merit. Note: The only issue I see with this is if the embedded code-cell is not at the end of the exercise block then we need to track exercise content across code-cells in the notebook for formatting.
@choldgraf just to clarify one point on your suggestion. Would all the jupyter-sphinx directives be executed by the same independent kernel (at the file level)?
Perhaps we can introduce an {exercise-code} node that uses jupyter-sphinx under the hood
Then we can add support for hidden etc. so that you could have a preamble / import blocks for all exercises that doesn't show in outputs. We could also support future features like exercise/solution notebooks. From a user perspective I think it would be really confusing to be able to use jupyter-sphinx but not code-cell so perhaps exercise-code would make that relationship clearer at the expense of being a lightweight wrapper.
One con however is that these nodes require sphinx to be interpreted / executed -- so if we are to support download notebooks for websites (or binder / runnable notebooks) it will be difficult to have these nested items supported in any way in the notebook format.
I think exercise-code could be a good way to make it explicit. Your point about not having the same structure when you export to ipynb is precisely the challenge here, I think. Jupyter cache wants to have the exact same notebook in the cached and “source” version, which is why I believe @chrisjsewell has pushed back on requests to transform the content of a source file (like by in-nesting) in order to be executable as a notebook.
Longer term, I feel like we’re really going to need to come up with some kind of story for the non-linear (eg nested) execution workflow. It’s common and popular in R, and many services nowadays are supporting this kind of thing. I still wonder if we can accomplish this with improving the UX around glue-like functionality 🤔🤔🤔
@choldgraf@chrisjsewell I am going to dive into this issue. I think using this as a prototype to figure out the best way to setup nested code will be useful case study to discuss the broader issue of nested code-cells in jupyterbook. Perhaps once a prototype is up and running we can all catchup on a technical workshop style meeting to see if we like it or can think of a better solution.
I will work with @chrisjsewell suggestion of using directives to indicate a start and stop while keeping code-cell at the root level for compatibility with myst and jupytext etc.
@AakashGfude thanks for the brainstorming today. It looks like we could support styling only through the use of visit_solution_start and visit_solution_end nodes injecting updates to html and latex but it will be difficult to support features like hidden.
I think we will need to do some parsing of the doctree to support a full suite of features.
Perhaps we should use a transform to alter the tree (early on in the parsing) to build a parent-child relationship with nodes that are contained between solution-start and solution-end. But this transform would have to occur after md -> ipynb to be fully compliant with jupytext etc.
So we need to latch in at:
md -> ipynb -> jupyter-cache
read ipynb sources via (myst_nb)
apply sphinx-exercise transform to make nodes between solution-start and solution-end children of a solution node for processing by sphinx -> html, latex
parse as before extracting any code-cell outputs from jupyter-cache
@chrisjsewell@AakashGfude I think a transform is needed here to provide sphinx the relationship between the nodes. What do you think?
changed the title [-]Embed executable code inside of exercise solutions[/-][+]Embed executable code inside of exercise solutions (Prototyping)[/+]on Aug 8, 2021
I see two syntax options when building out tests for this.
Playing around the two syntax choices I think I am actually in favour of option 1 as I think it will be harder to miss a start and an end marker. I was initially thinking option 2 would be nice as you then only need to know one directive name but probably needs a lot more error checking.
Option 1:
```{solution-start} exercise-1
:label: solution-1
```
```{code-cell}
import numpy as np
from scipy.interpolate import splprep, splev
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import PathPatch
N = 400
t = np.linspace(0, 2 * np.pi, N)
r = 0.5 + np.cos(t)
x, y = r * np.cos(t), r * np.sin(t)
fig, ax = plt.subplots()
ax.plot(x, y)
plt.show()
```
```{solution-end} exercise-1
```
Option 2:
```{solution} exercise-1
:label: solution-1
:start:
```
```{code-cell}
import numpy as np
from scipy.interpolate import splprep, splev
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import PathPatch
N = 400
t = np.linspace(0, 2 * np.pi, N)
r = 0.5 + np.cos(t)
x, y = r * np.cos(t), r * np.sin(t)
fig, ax = plt.subplots()
ax.plot(x, y)
plt.show()
```
```{solution} exercise-1
:end:
```
Just checking: I hope with this change in place, the original single directive will still stay, is this correct?
thanks @akhmerov -- for sure -- not thinking we would remove the single directive exercise but rather support code-cell through a gated directive approach (as above) which can then map 1-to-1 through jupytext and get executed etc.
I quite like the gated directive approach as it tends to simplify the syntax (when compared with nested structures) at the cost of searching from -start and -end components.
Activity
jstac commentedon Oct 10, 2020
Thanks for opening @choldgraf .
I would find option 1 easier to remember.
One related alternative would be to replace
solution-startwithsolution-headerand dropsolution-stop.Then the only think that's being inserted is a heading that tells us what exercise is being solved, while the contents of the solution is just ordinary notebook content.
I suppose the exercise could be symmetric, with
exercise-headerinserting "Exercise x", typeset nicely as determined by the extension, and the content of the exercise again being ordinary notebook content underneath the header.najuzilu commentedon Oct 13, 2020
These are great suggestions @choldgraf @jstac.
Just to double check, it wouldn't be possible to automate the gluing functionality so that whenever a
code-cellis introduced in the content of the directive, it is replaced with a code block and the output of the code-cell - which can be added at the bottom of the document with aremove-celltag - is glued in the directive?The only issue I have with @jstac's
solution-headersuggestion is that we wouldn't be able to duplicate/copy solutions to another location as suggested in issue #11.mmcky commentedon Nov 5, 2020
One downside here would be if you wanted to apply styling to the
solutiontext (i.e. in similar fashion to an admonition). I think it would be useful to have a end demarcation so that the solution block is identified in theast.This is actually similar to the root issue behind executablebooks/MyST-NB#274. The motivation behind this request is to be able to bring in code from a file (as used to be done with
literalinclude) but jupytext (rightly) has no knowledge of anything outside ofcode-cellbreaks.I am in favour of
option1style approach to this -- mainly because the content stays ordered. If we introducejupytextfilters as a way to update content such as links to a website (instead of to other notebooks) to support download notebooks for websites then having notebooks with content in the correct order will make this work easier.mmcky commentedon Nov 5, 2020
Could we have an argument
startandend-- rather than two different directives:I guess we should also think about other cases where nested
code-cellmay not work as we will want to adopt a consistent approach to solving this issue. The only other that comes to mind is perhapstabbedblocks -- but that syntax would get pretty messy if we were to add something like this totabbedjstac commentedon Nov 5, 2020
Thanks for your thoughts @mmcky .
As a user, I would be happy enough with the original option 1, although I do see your point about other nested code cells and the need for a consistent approach. Option 1 doesn't feel consistent with the rest of myst.
jstac commentedon Nov 25, 2020
Hi guys, tossing out an alternative option: How about we use tabs? (as in https://jupyterbook.org/content/content-blocks.html#tabbed-content)
One tab is "Exercise X" and the other tab, with content initially hidden, is "Solution".
mmcky commentedon Nov 25, 2020
hey @jstac this is a nice format option -- I like it. I think we will still get issues with executing the nested code-blocks (cc: @chrisjsewell) as it breaks the linear document
jupyter notebookrule for the code-blocks though.mmcky commentedon Nov 25, 2020
I wonder if we could have
tabbedcode blocks in jupyter notebooks which had an execution rule oflefttoright. This would then directly translate to the notebooks format but this would essentially require a jupyter extension I think to change thebasejupyter notebook format.choldgraf commentedon Nov 25, 2020
Just a note as well that we can always use
jupyter-sphinxto execute and embed outputs in the built docs, and we can embedjupyter-sphinxexecution inside of admonitions, tabs, etc. So you could have a MyST-NB notebook that also hasjupyter-sphinxdirectives in it, and the{code-cell}blocks would just be executed separately from thejupyter-sphinxexecution. The only challenge is that jupyter-sphinx won't have access to the same kernel thatjupyter-cacheuses when it executes the notebook. But this might still work for certain kinds of exercises.mmcky commentedon Dec 10, 2020
thanks @choldgraf -- that is a thought I had as well and might be the best we can do (unless
jupyter-cacheimplements something like pre-execution filters/transforms). My concern though is the document will necessarily need more imports and it "looks" like it would flow through the document linearly -- when in fact execution is not. It also complicates the production of supportdownload notebooks.In
sphinxcontrib-jupyterthe code-block (nestedness) was ignored and a code-cell block would just be written following theexercisebut we can't do that here as the action takes place injupyter-cachefirst.What do you think about a
jupyter-cachetransform that could look for any nested code-cells and promote them to be code-cells where they occur:{code-cell}occurs in a markdown block it would (a) split the markdown block, (b) copy the contents ofcode-cellto a code-cell in the notebookjson, (b) remove the{code-cell}from the text.Given there is no real
translationjust a simple parse task -- this may have some merit. Note: The only issue I see with this is if the embeddedcode-cellis not at the end of the exercise block then we need to trackexercisecontent acrosscode-cellsin the notebook for formatting.mmcky commentedon Dec 10, 2020
@choldgraf just to clarify one point on your suggestion. Would all the
jupyter-sphinxdirectives be executed by the sameindependentkernel (at the file level)?Perhaps we can introduce an
{exercise-code}node that usesjupyter-sphinxunder the hoodThen we can add support for
hiddenetc. so that you could have a preamble / import blocks for all exercises that doesn't show in outputs. We could also support future features likeexercise/solution notebooks. From a user perspective I think it would be really confusing to be able to usejupyter-sphinxbut notcode-cellso perhapsexercise-codewould make that relationship clearer at the expense of being a lightweight wrapper.One
conhowever is that these nodes requiresphinxto be interpreted / executed -- so if we are to supportdownload notebooksfor websites (or binder / runnable notebooks) it will be difficult to have these nested items supported in any way in the notebook format.choldgraf commentedon Dec 12, 2020
I think exercise-code could be a good way to make it explicit. Your point about not having the same structure when you export to ipynb is precisely the challenge here, I think. Jupyter cache wants to have the exact same notebook in the cached and “source” version, which is why I believe @chrisjsewell has pushed back on requests to transform the content of a source file (like by in-nesting) in order to be executable as a notebook.
Longer term, I feel like we’re really going to need to come up with some kind of story for the non-linear (eg nested) execution workflow. It’s common and popular in R, and many services nowadays are supporting this kind of thing. I still wonder if we can accomplish this with improving the UX around glue-like functionality 🤔🤔🤔
mmcky commentedon Feb 22, 2021
@choldgraf @chrisjsewell I am going to dive into this issue. I think using this as a prototype to figure out the best way to setup
nested codewill be useful case study to discuss the broader issue ofnested code-cells in jupyterbook. Perhaps once a prototype is up and running we can all catchup on atechnical workshopstyle meeting to see if we like it or can think of a better solution.I will work with @chrisjsewell suggestion of using
directivesto indicate a start and stop while keepingcode-cellat the root level for compatibility withmystandjupytextetc.Such as:
The current
solutiondirective includes support for:label,classandhiddenattributescc: @jstac
mmcky commentedon Feb 22, 2021
@AakashGfude thanks for the brainstorming today. It looks like we could support
stylingonly through the use ofvisit_solution_startandvisit_solution_endnodes injecting updates tohtmlandlatexbut it will be difficult to support features likehidden.I think we will need to do some parsing of the
doctreeto support a full suite offeatures.Perhaps we should use a
transformto alter the tree (early on in the parsing) to build aparent-childrelationship withnodesthat are contained betweensolution-startandsolution-end. But this transform would have to occur aftermd -> ipynbto be fully compliant withjupytextetc.So we need to latch in at:
md->ipynb->jupyter-cacheipynbsources via (myst_nb)sphinx-exercisetransform to make nodes betweensolution-startandsolution-endchildren of asolutionnode for processing bysphinx->html,latexcode-celloutputs fromjupyter-cache@chrisjsewell @AakashGfude I think a
transformis needed here to providesphinxthe relationship between thenodes. What do you think?[-]Embed executable code inside of exercise solutions[/-][+]Embed executable code inside of exercise solutions (Prototyping)[/+]mmcky commentedon Oct 27, 2021
Update: 27th October 2021.
I see two syntax options when building out tests for this.
Playing around the two syntax choices I think I am actually in favour of
option 1as I think it will be harder to miss astartand anendmarker. I was initially thinkingoption 2would be nice as you then only need to know onedirectivename but probably needs a lot more error checking.Option 1:
Option 2:
akhmerov commentedon Oct 27, 2021
Just checking: I hope with this change in place, the original single directive will still stay, is this correct?
mmcky commentedon Oct 27, 2021
thanks @akhmerov -- for sure -- not thinking we would remove the single directive
exercisebut rather supportcode-cellthrough a gated directive approach (as above) which can then map 1-to-1 throughjupytextand get executed etc.mmcky commentedon Dec 7, 2021
I have now setup an working prototype of this idea for
sphinx-exercise#45
I quite like the gated directive approach as it tends to simplify the syntax (when compared with nested structures) at the cost of searching from
-startand-endcomponents.