Newer
Older
# Snipper
A lightweight framework for removing code from student solutions.
## Installation
pip install codesnipper
```
## What it does
This project address the following three challenges for administering a python-based course
- You need to maintain a (working) version for debugging as well as a version handed out to students (with code missing)
- You ideally want to make references in source code to course material *"(see equation 2.1 in exercise 5)"* but these tend to go out of date
- You want to include code snippets and code output in lectures notes/exercises/beamer slides
- You want to automatically create student solutions
This framework address these problems and allow you to maintain a **single**, working project repository.
The project is currently used in **02465** at DTU. An example of student code can be found at:
- https://gitlab.gbar.dtu.dk/02465material/02465students/blob/master/irlc/ex02/dp.py
A set of lectures notes where all code examples/output are automatically generated from the working repository can be found a
- https://lab.compute.dtu.dk/tuhe/books (see **Sequential decision making**)
# Usage
All examples can be found in the `/examples` directory. The idea is all our (complete) files are found in the instructor directory and snipper keeps everything up-to-date:
```text
examples/cs101_instructor # This directory contains the (hidden) instructor files. You edit these
examples/cs101_students # This directory contains the (processed) student files. Don't edit these
examples/cs101_output # This contains automatically generated contents (snippets, etc.).
```
The basic functionality is you insert special comment tags in your source, such as `#!b` or `#!s` and the script then process
the sources based on the tags. The following will show most basic usages:
## The #f!-tag
Let's start with the simplest example, blocking out a function (see `examples/cs101_instructor/f_tag.py`; actually it will work for any scope)
You insert a comment like: `#!f <exception message>` like so:
def myfun(a,b): #!f return the sum of a and b
""" The doc-string is not removed. """
sm = a+b
return sm
To compile this (and all other examples) use the script `examples/process_cs101.py`
from snipper.snip_dir import snip_dir
snip_dir("./cs101_instructor", "./cs101_students", output_dir="./cs101_output")
```
The output can be found in `examples/students/f_tag.py`. It will cut out the body of the function but leave any return statement and docstrings. It will also raise an exception (and print how many lines are missing) to help students.
```python
"""
"""
def myfun(a,b):
""" The doc-string is not removed. """
# TODO: 1 lines missing.
raise NotImplementedError("return the sum of a and b")
return sm
## The #b!-tag
The #!b-tag allows you more control over what is cut out. The instructor file:
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def primes_sieve(limit):
limitn = limit+1 #!b
primes = range(2, limitn)
for i in primes:
factors = list(range(i, limitn, i))
for f in factors[1:]:
if f in primes:
primes.remove(f) #!b Compute the list `primes` here of all primes up to `limit`
return primes
width, height = 2, 4
print("Area of square of width", width, "and height", height, "is:")
print(width*height) #!b #!b Compute and print area here
print("and that is a fact!")
```
Is compiled into:
```python
"""
"""
def primes_sieve(limit):
# TODO: 8 lines missing.
raise NotImplementedError("Compute the list `primes` here of all primes up to `limit`")
return primes
width, height = 2, 4
print("Area of square of width", width, "and height", height, "is:")
# TODO: 1 lines missing.
raise NotImplementedError("Compute and print area here")
print("and that is a fact!")
```
This allows you to cut out text across scopes, but still allows you to insert exceptions.
## The #s!-tag
The #!s-tag is useful for making examples to include in exercises and lecture notes. The #!s (snip) tag cuts out the text between
tags and places it in files found in the output-directory. As an example, here is the instructor file:
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
width, height = 2, 4
print("Area of square of width", width, "and height", height, "is:") #!s
print(width*height) #!s # This is an example of a simple cutout
print("and that is a fact!")
print("An extra cutout") #!s #!s # This will be added to the above cutout
def primes_sieve(limit): #!s=a # A named cutout
limitn = limit+1
primes = range(2, limitn)
for i in primes: #!s=b A nested/named cutout.
factors = list(range(i, limitn, i))
for f in factors[1:]:
if f in primes:
primes.remove(f) #!s=b
return primes #!s=a
```
Note it allows
- naming using the #!s=<name> command
- automatically join snippets with the same name (useful to cut out details)
- The named tags will be matched, and do not have to strictly contained in each other
This example will produce three files
`cs101_output/s_tag.py`, `cs101_output/s_tag_a.py`, and `cs101_output/s_tag_b.py` containing the output:
```python
# s_tag.py
print("Area of square of width", width, "and height", height, "is:")
print(width*height) #!s # This is an example of a simple cutout
print("and that is a fact!")
print("An extra cutout") #!s #!s # This will be added to the above cutout
def primes_sieve(limit):
print(width*height)
print("and that is a fact!")
print("An extra cutout") #!s #!s # This will be added to the above cutout
def primes_sieve(limit):
limitn = limit+1
primes = range(2, limitn)
for i in primes:
print("An extra cutout")
def primes_sieve(limit):
limitn = limit+1
primes = range(2, limitn)
for i in primes:
factors = list(range(i, limitn, i))
for f in factors[1:]:
if f in primes:
primes.remove(f)
print("An extra cutout")
def primes_sieve(limit):
limitn = limit+1
primes = range(2, limitn)
for i in primes:
factors = list(range(i, limitn, i))
for f in factors[1:]:
if f in primes:
primes.remove(f)
return primes
```
and
```python
```
and finally:
```python
```
I recommend using `\inputminted{filename}` to insert the cutouts in LaTeX.
## The #o!-tag
The #!o-tag allows you to capture output from the code, which can be useful when showing students the expected
behavior of their scripts. Like the #!s-tag, the #!o-tags can be named.
As an example, Consider the instructor file
```python
if __name__ == "__main__":
print("Here are the first 4 square numbers") #!o=a
for k in range(1,5):
print(k*k, "is a square")
#!o=a
print("This line will not be part of a cutout.")
width, height = 2, 4 #!o=b
print("Area of square of width", width, "and height", height, "is:")
print(width*height)
print("and that is a fact!") #!o=b
```
This example will produce two files `cs101_output/o_tag_a.txt`, `cs101_output/o_tag_b.txt`:
```python
```
and
```python
```
## The #i!-tag
The #!i-tag allows you to create interactive python shell-snippets that can be imported using the minted `pycon` environment ('\inputminted{python}{input.shell}').
As an example, consider the instructor file
```python
for animal in ["Dog", "cat", "wolf"]: #!i=a
print("An example of a four legged animal is", animal) #!i=a
#!i=b
def myfun(a,b):
return a+b
myfun(3,4) #!i=b
# Snipper will automatically insert an 'enter' after the function definition.
```
This example will produce two files `cs101_output/i_tag_a.shell`, `cs101_output/i_tag_b.shell`:
and
```pycon
```
Note that apparently there
is no library for converting python code to shell sessions so I had to write it myself, which means it can properly get confused with multi-line statements (lists, etc.). On the plus-side, it will automatically insert newlines after the end of scopes.
My parse is also known to be a bit confused if your code outputs `...` since it has to manually parse the interactive python session and this normally indicates a new line.
## References and citations (`\ref` and `\cite`)
One of the most annoying parts of maintaining student code is to constantly write "see equation on slide 41 bottom" only to have the reference go stale because slide 23 got removed. Well now anymore, now you can direcly refence anything with a bibtex or aux file!
Let's consider the following example of a simple document with a couple of references: (see `examples/latex/index.pdf`):

Bibliography references can be loaded from `references.bib`-files and in-document references from the `.aux` file.
For this example, we will insert references shown in the `examples/latex/index.tex`-document. To do so, we can use these tags:
```python
def myfun(): #!s
"""
To solve this exercise, look at \ref{eq1} in \ref{sec1}.
You can also look at \cite{bertsekasII} and \cite{herlau}
More specifically, look at \cite[Equation 117]{bertsekasII} and \cite[\ref{fig1}]{herlau}
We can also write a special tag to reduce repetition: \nref{fig1} and \nref{sec1}.
"""
return 42 #!s
```
We can manually compile this example by first loading the aux-files and the bibliographies as follows:
```python
```
Next, we load the python file containing the reference code and fix all references based on the aux and bibliography data.
```python
```
The middle command is a convenience feature: It allows us to specify a special citation command `\nref{..}` which always compiles to `\cite[\ref{...}]{herlau}`. This is useful if e.g. `herlau` is the bibtex key for your lecture notes. The result is as follows:
```python
def myfun(): #!s
"""
To solve this exercise, look at \ref{eq1} in \ref{sec1}.
You can also look at \cite{bertsekasII} and \cite{herlau}
More specifically, look at \cite[Equation 117]{bertsekasII} and \cite[\ref{fig1}]{herlau}
We can also write a special tag to reduce repetition: \nref{fig1} and \nref{sec1}.
"""
return 42 #!s
```
Note this example uses the low-level api. Normally you would just pass the bibtex and aux-file to the main censor-file command.
- You can name tags using `#!s=bar` to get a `foo_bar.py` snippet. This is useful when you need to cut multiple sessions. This also works for the other tags.