Skip to content

Commit

Permalink
Add solve command (#41)
Browse files Browse the repository at this point in the history
* Add solve functionality.

* Add solutions creation to tutorial.

* Add solve cli command.

* Run black.

* Fix error in docs.
  • Loading branch information
drvinceknight committed Dec 9, 2020
1 parent 2e6a5a4 commit 9bf0606
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/howto/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ How to:
install.rst
write_an_assignment.rst
release_a_source_assignment.rst
solve_an_assignment.rst
check_a_submission.rst
contribute.rst
42 changes: 42 additions & 0 deletions docs/howto/solve_an_assignment.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
solve an assignment
===================

You can solve an assignment in 1 or 2 ways:

1. Using the command line tool.
2. Using :code:`nbchkr` as a library.

Using the command line tool
---------------------------

Given a source assignment :code:`main.ipynb`::

$ nbchkr solve --source main.ipynb --output solution.ipynb

This creates :code:`solution.ipynb` with relevant cells removed which can
then be distributed to students.

Using :code:`nbchkr` as a library
---------------------------------

All of :code:`nbchkr`'s functionality is exposed to the user as a library.

Importing the relevant libraries::

>>> import pathlib
>>> import re
>>> import nbchkr.utils

Reading in the source notebook :code:`main.ipynb` and removing relevant cells.
We here use a regex that matches nothing for the solutions (as we want them to
stay in place)::

>>> nb_path = pathlib.Path("main.ipynb")
>>> solution_regex = re.compile('$^')
>>> nb_node = nbchkr.utils.read(nb_path=nb_path)
>>> student_nb = nbchkr.utils.remove_cells(nb_node=nb_node, solution_regex=solution_regex)

Writing the assignment notebooks :code:`assignment.ipynb`::

>>> output_path = pathlib.Path("solution.ipynb")
>>> nbchkr.utils.write(output_path=output_path, nb_node=nb_node)
1 change: 1 addition & 0 deletions docs/tutorial/assignment/solution.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"cells": [{"cell_type": "markdown", "metadata": {}, "source": "# Class assignment\n\nWe will use this assignment to solidify our understanding of using Python to\ncarry out some numerical operations and also write functions.\n\nUse this notebook to write your answers in the cells as instructed, do your\nbest not to delete any of the cells that are already there.\n\n## Question one.\n\nUse python to obtain the remainder when dividing 21 by 5:\n\n\\\\[21 \\mod 5\\\\]"}, {"cell_type": "code", "execution_count": 1, "metadata": {"tags": ["answer:q1"]}, "outputs": [{"data": {"text/plain": "1"}, "execution_count": 1, "metadata": {}, "output_type": "execute_result"}], "source": "### BEGIN SOLUTION\n21 % 5\n### END SOLUTION"}, {"cell_type": "markdown", "metadata": {}, "source": "## Question two.\n\nWrite a python function `get_remainder(m, n)` that returns the remainder\nthe remainder when dividing \\\\(m\\\\) by \\\\(n\\\\).\n\n\\\\[m \\mod n\\\\]"}, {"cell_type": "code", "execution_count": 4, "metadata": {"tags": ["answer:q2"]}, "outputs": [], "source": "def get_remainder(m, n):\n ### BEGIN SOLUTION\n \"\"\"\n This function returns the remainder of m when dividing by n\n \"\"\"\n return m % n\n ### END SOLUTION"}], "metadata": {"celltoolbar": "Tags", "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.1"}}, "nbformat": 4, "nbformat_minor": 4}
11 changes: 11 additions & 0 deletions docs/tutorial/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,17 @@ to students. To do this, we use the command line tool that comes with :code:`nbc
This creates :download:`assignment.ipynb <./assignment/assignment.ipynb>` with
the answers and checks removed.

Releasing solutions
-------------------

If we want to create a model solution we can.
To do this, we use the command line tool that comes with :code:`nbchkr`::

$ nbchkr solve --source main.ipynb --output solution.ipynb

This creates :download:`solution.ipynb <./assignment/solution.ipynb>` with
the checks removed.

Checking student assignments and generating feedback
----------------------------------------------------

Expand Down
27 changes: 25 additions & 2 deletions src/nbchkr/__main__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import csv
import glob
import pathlib
import re

import typer

Expand All @@ -17,15 +18,37 @@ def release(
),
):
"""
This releases a piece of coursework by removing the solutions from a source.
This releases a piece of coursework by removing the solutions and checks from a source.
"""
nb_path = pathlib.Path(source)
nb_node = nbchkr.utils.read(nb_path=nb_path)
nbchkr.utils.remove_cells(nb_node=nb_node)

output_path = pathlib.Path(output)
nbchkr.utils.write(output_path=output_path, nb_node=nb_node)
typer.echo(f"Solutions removed from {source}. New notebook written to {output}.")
typer.echo(
f"Solutions and checks removed from {source}. New notebook written to {output}."
)


@app.command()
def solve(
source: pathlib.Path = typer.Option(..., help="The path to the source ipynb file"),
output: pathlib.Path = typer.Option(
..., help="The path to the destination ipynb file"
),
):
"""
This solves a piece of coursework by removing the checks from a source.
"""
solution_regex = re.compile("$^") # Matches nothing
nb_path = pathlib.Path(source)
nb_node = nbchkr.utils.read(nb_path=nb_path)
nbchkr.utils.remove_cells(nb_node=nb_node, solution_regex=solution_regex)

output_path = pathlib.Path(output)
nbchkr.utils.write(output_path=output_path, nb_node=nb_node)
typer.echo(f"Checks removed from {source}. New notebook written to {output}.")


@app.command()
Expand Down
59 changes: 55 additions & 4 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ def test_help_call():
Commands:
check This checks a given submission against a source.
release This releases a piece of coursework by removing the solutions...
release This releases a piece of coursework by removing the solutions
and...
solve This solves a piece of coursework by removing the checks from a...
"""
assert output.stdout == expected_stdout
assert output.stderr == b""
Expand All @@ -34,8 +37,23 @@ def test_release_help_call():
output = subprocess.run(["nbchkr", "release", "--help"], capture_output=True)
expected_stdout = b"""Usage: nbchkr release [OPTIONS]
This releases a piece of coursework by removing the solutions from a
source.
This releases a piece of coursework by removing the solutions and checks
from a source.
Options:
--source PATH The path to the source ipynb file [required]
--output PATH The path to the destination ipynb file [required]
--help Show this message and exit.
"""
assert output.stdout == expected_stdout
assert output.stderr == b""


def test_solve_help_call():
output = subprocess.run(["nbchkr", "solve", "--help"], capture_output=True)
expected_stdout = b"""Usage: nbchkr solve [OPTIONS]
This solves a piece of coursework by removing the checks from a source.
Options:
--source PATH The path to the source ipynb file [required]
Expand Down Expand Up @@ -83,7 +101,7 @@ def test_release():
capture_output=True,
)
expected_stdout = str.encode(
f"Solutions removed from {NB_PATH}/test.ipynb. New notebook written to student.ipynb.\n"
f"Solutions and checks removed from {NB_PATH}/test.ipynb. New notebook written to student.ipynb.\n"
)
assert output.stderr == b""
assert output.stdout == expected_stdout
Expand All @@ -100,6 +118,39 @@ def test_release():
pass


def test_solve():
# TODO Add better tear down.
output = subprocess.run(
[
"nbchkr",
"solve",
"--source",
f"{NB_PATH}/test.ipynb",
"--output",
"solution.ipynb",
],
capture_output=True,
)
expected_stdout = str.encode(
f"Checks removed from {NB_PATH}/test.ipynb. New notebook written to solution.ipynb.\n"
)
assert output.stderr == b""
assert output.stdout == expected_stdout

student_nb = nbchkr.utils.read(nb_path="solution.ipynb")
expected_length = 4
assert type(student_nb["cells"]) is list
assert len(student_nb["cells"]) == expected_length
assert "assert" not in str(student_nb)
assert "sum(i for i in range(11))" in str(student_nb)
# TODO Add a better pytest cleanup.
try:
pathlib.Path("solution.ipynb").unlink()
except FileNotFoundError: # TODO Ensure py3.8 is used so that can pass
# `missing_ok=True` to `path.unlink`.
pass


def test_check_on_a_single_notebook():
# TODO add better tear down.

Expand Down

0 comments on commit 9bf0606

Please sign in to comment.