Skip to content
Snippets Groups Projects
Commit 7e820f22 authored by tuhe's avatar tuhe
Browse files

Updates

parent 8647ebf0
No related branches found
No related tags found
No related merge requests found
Showing
with 792 additions and 105 deletions
LICENSE 0 → 100644
Copyright (c) 2018 The Python Packaging Authority
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
include *.dat
...@@ -4,47 +4,35 @@ Unitgrade is an automatic report and exam evaluation framework that enables inst ...@@ -4,47 +4,35 @@ Unitgrade is an automatic report and exam evaluation framework that enables inst
Unitgrade is build on pythons `unittest` framework so that the tests can be specified in a familiar syntax and will integrate with any modern IDE. What it offers beyond `unittest` is the ability to collect tests in reports (for automatic evaluation) and an easy and 100% safe mechanism for verifying the students results and creating additional, hidden tests. A powerful cache system allows instructors to automatically create test-answers based on a working solution. Unitgrade is build on pythons `unittest` framework so that the tests can be specified in a familiar syntax and will integrate with any modern IDE. What it offers beyond `unittest` is the ability to collect tests in reports (for automatic evaluation) and an easy and 100% safe mechanism for verifying the students results and creating additional, hidden tests. A powerful cache system allows instructors to automatically create test-answers based on a working solution.
- 100% Python `unittest` compatible - 100% Python `unittest` compatible
- No external configuration files: Just write a `unittest` - No external configuration files, just write a `unittest`
- No unnatural limitations: Use any package or framework. If you can `unittest` it, it works. - No unnatural limitations: If you can `unittest` it, it works.
- Granular security model: - Granular security model:
- Students get public `unittests` for easy development of solutions - Students get public `unittests` for easy development of solutions
- Students get a tamper-resistant file to create submissions which are uploaded - Students get a tamper-resistant file to create submissions which are uploaded
- Instructors can automatically verify the students solution using a Docker VM and run hidden tests - Instructors can automatically verify the students solution using Docker VM and by running hidden tests
- Allow export of assignments to Autolab (no `make` file mysteries!)
- Tests are quick to run and will integrate with your IDE - Tests are quick to run and will integrate with your IDE
## Installation ## Installation
Unitgrade can be installed through pip using Unitgrade can be installed using `pip`:
``` ```
pip install git+https://git@gitlab.compute.dtu.dk/tuhe/unitgrade.git pip install unitgrade
``` ```
This will install unitgrade in your site-packages directory. If you want to upgrade an old installation of unitgrade: This will install unitgrade in your site-packages directory. If you want to upgrade an old installation of unitgrade:
``` ```
pip install git+https://git@gitlab.compute.dtu.dk/tuhe/unitgrade.git --upgrade pip install unitgrade --upgrade
``` ```
If you are using anaconda+virtual environment you can install it as If you are using anaconda+virtual environment you can install it as
``` ```
source activate myenv source activate myenv
conda install git pip conda install git pip
pip install git+https://git@gitlab.compute.dtu.dk/tuhe/unitgrade.git pip install unitgrade
``` ```
Alternatively, simply use git-clone of the sources and add unitgrade to your python path.
When you are done, you should be able to import unitgrade: When you are done, you should be able to import unitgrade:
``` ```
import unitgrade import unitgrade
``` ```
## Testing installation
I have provided an example project which illustrates all main features in a self-contained manner and which should
work immediately upon installation. The source can be found here: https://lab.compute.dtu.dk/tuhe/unitgrade/-/tree/master/cs101courseware_example
To run the example, first start a python console:
```
python
```
Then run the code
```
from cs101courseware_example import instructions
```
This will print on-screen instructions for how to use the system tailored to your user-specific installation path.
## Evaluating a report ## Evaluating a report
Homework is broken down into **reports**. A report is a collection of questions which are individually scored, and each question may in turn involve multiple tests. Each report is therefore given an overall score based on a weighted average of how many tests are passed. Homework is broken down into **reports**. A report is a collection of questions which are individually scored, and each question may in turn involve multiple tests. Each report is therefore given an overall score based on a weighted average of how many tests are passed.
...@@ -83,12 +71,12 @@ To register your results, please run the file: ...@@ -83,12 +71,12 @@ To register your results, please run the file:
>>> cs101report1_grade.py >>> cs101report1_grade.py
In the same manner as you ran this file. In the same manner as you ran this file.
``` ```
Once you are happy with the result, run the alternative, not-easy-to-tamper-with script called `cs101report1_grade.py`: Once you are happy with the result run the script with the `_grade.py`-postfix, in this case `cs101report1_grade.py`:
``` ```
python cs101report1_grade.py python cs101report1_grade.py
``` ```
This runs the same tests, and generates a file `Report0_handin_18_of_18.token`. The file name indicates how many points you got. Upload this file to campusnet. This runs the same tests, and generates a file `Report0_handin_18_of_18.token`. The file name indicates how many points you got. Upload this file to campusnet (and no other).
### Why are there two scripts? ### Why are there two scripts?
The reason why we use a standard test script, and one with the `_grade.py` extension, is because the tests should both be easy to debug, but at the same time we have to prevent accidential changes to the test scripts. Hence, we include two versions of the tests. The reason why we use a standard test script, and one with the `_grade.py` extension, is because the tests should both be easy to debug, but at the same time we have to prevent accidential changes to the test scripts. Hence, we include two versions of the tests.
...@@ -97,7 +85,7 @@ The reason why we use a standard test script, and one with the `_grade.py` exten ...@@ -97,7 +85,7 @@ The reason why we use a standard test script, and one with the `_grade.py` exten
- **My non-grade script and the `_grade.py` script gives different number of points** - **My non-grade script and the `_grade.py` script gives different number of points**
Since the two scripts should contain the same code, the reason is nearly certainly that you have made an (accidental) change to the test scripts. Please ensure both scripts are up-to-date and if the problem persists, try to get support. Since the two scripts should contain the same code, the reason is nearly certainly that you have made an (accidental) change to the test scripts. Please ensure both scripts are up-to-date and if the problem persists, try to get support.
- **Why is there a `*_resources_do_not_hand_in.dat` file? Should I also upload it?** - **Why is there a `unitgrade` directory with a bunch of pickle files? Should I also upload them?**
No. The file contains the pre-computed test results your code is compared against. If you want to load this file manually, the unitgrade package contains helpful functions for doing so. No. The file contains the pre-computed test results your code is compared against. If you want to load this file manually, the unitgrade package contains helpful functions for doing so.
- **I am worried you might think I cheated because I opened the '_grade.py' script/token file** - **I am worried you might think I cheated because I opened the '_grade.py' script/token file**
...@@ -135,7 +123,7 @@ Yes. ...@@ -135,7 +123,7 @@ Yes.
That the script `report1_grade.py` is difficult to read is not the principle safety measure. Instead, it ensures there is no accidential tampering. If you muck around with these files and upload the result, we will very likely know. That the script `report1_grade.py` is difficult to read is not the principle safety measure. Instead, it ensures there is no accidential tampering. If you muck around with these files and upload the result, we will very likely know.
- **I have private data on my computer. Will this be read or uploaded?** - **I have private data on my computer. Will this be read or uploaded?**
No. The code will look for and upload your solutions, but it will not read/look at other directories in your computer. In the example provided with this code, this means you should expect unitgrade to read/run all files in the `cs101courseware_example`-directory, but **no** other files on your computer (unless some code in this directory load other files). So as long as you keep your private files out of the base courseware directory, you should be fine. No. The code will look for and upload your solutions, but it will not read/look at other directories in your computer. In the example provided with this code, this means you should expect unitgrade to read/run all files in the `cs101courseware_example`-directory, but **no** other files on your computer. So as long as you keep your private files out of the base courseware directory, you should be fine.
- **Does this code install any spyware/etc.? Does it communicate with a website/online service?** - **Does this code install any spyware/etc.? Does it communicate with a website/online service?**
No. Unitgrade makes no changes outside the courseware directory and it does not do anything tricky. It reads/runs code and write the `.token` file. No. Unitgrade makes no changes outside the courseware directory and it does not do anything tricky. It reads/runs code and write the `.token` file.
......
...@@ -18,7 +18,7 @@ for d in os.listdir(wdir): ...@@ -18,7 +18,7 @@ for d in os.listdir(wdir):
if "__" not in d and d != "instructions.py": if "__" not in d and d != "instructions.py":
print("> ", d) print("> ", d)
print("") print("")
fprint("The file homework1.py is the file you edit as part of the course; you are welcome to open it and inspect the content, but right now it consists of some simple programming tasks plus instructions.") fprint("The file looping.py is the file you edit as part of the course; you are welcome to open it and inspect the content, but right now it consists of some simple programming tasks plus instructions.")
fprint("The file cs101report1.py contains the actual tests of the program. All the tests are easily readable and the script will work with your debugger if you are using pycharm, however, we will run the script for the command line. ") fprint("The file cs101report1.py contains the actual tests of the program. All the tests are easily readable and the script will work with your debugger if you are using pycharm, however, we will run the script for the command line. ")
fprint("To do so, open a console, and change directory to the cs103 main directory using e.g.:") fprint("To do so, open a console, and change directory to the cs103 main directory using e.g.:")
tprint(f'cd "{wdir}"') tprint(f'cd "{wdir}"')
......
File added
File added
File deleted
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"
\ No newline at end of file
from setuptools import setup # Use this guide:
from unitgrade.version import __version__ # https://packaging.python.org/tutorials/packaging-projects/
setup(
name='unitgrade', # from unitgrade2.version import __version__
import setuptools
with open("src/unitgrade2/version.py", "r", encoding="utf-8") as fh:
__version__ = fh.read().split(" = ")[1].strip()[1:-1]
# long_description = fh.read()
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setuptools.setup(
name="unitgrade",
version=__version__, version=__version__,
packages=['unitgrade', 'cs101courseware_example'], author="Tue Herlau",
author_email="tuhe@dtu.dk",
description="A student homework/exam evaluation framework build on pythons unittest framework.",
long_description=long_description,
long_description_content_type="text/markdown",
url='https://lab.compute.dtu.dk/tuhe/unitgrade', url='https://lab.compute.dtu.dk/tuhe/unitgrade',
license='Apache', project_urls={
author='Tue Herlau', "Bug Tracker": "https://lab.compute.dtu.dk/tuhe/unitgrade/issues",
author_email='tuhe@dtu.dk', },
description='A lightweight student evaluation framework build on unittest', classifiers=[
include_package_data=True, "Programming Language :: Python :: 3",
install_requires=['numpy', 'jinja2', 'tabulate', 'sklearn', 'compress_pickle', "pyfiglet"], "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
package_dir={"": "src"},
packages=setuptools.find_packages(where="src"),
python_requires=">=3.8",
license="MIT",
install_requires=['numpy', 'tabulate', 'tqdm', "pyfiglet", "colorama", "coverage"],
) )
from unitgrade2.version import __version__
import os import os
# DONT't import stuff here since install script requires __version__ # DONT't import stuff here since install script requires __version__
...@@ -34,4 +33,4 @@ def cache_read(file_name): ...@@ -34,4 +33,4 @@ def cache_read(file_name):
else: else:
return None return None
from unitgrade2.unitgrade2 import Hidden, myround, mfloor, msum, Capturing, ActiveProgress from unitgrade2.unitgrade2 import myround, mfloor, msum, Capturing, ActiveProgress
File added
File added
...@@ -2,19 +2,13 @@ import numpy as np ...@@ -2,19 +2,13 @@ import numpy as np
from tabulate import tabulate from tabulate import tabulate
from datetime import datetime from datetime import datetime
import pyfiglet import pyfiglet
from unitgrade2 import Hidden, myround, msum, mfloor, ActiveProgress from unitgrade2 import msum
from unitgrade2 import __version__
import unittest import unittest
# from unitgrade2.unitgrade2 import MySuite
from unitgrade2.unitgrade2 import UTextResult from unitgrade2.unitgrade2 import UTextResult
import inspect import inspect
import os import os
import argparse import argparse
import sys
import time import time
import threading # don't import Thread bc. of minify issue.
import tqdm # don't do from tqdm import tqdm because of minify-issue
parser = argparse.ArgumentParser(description='Evaluate your report.', epilog="""Example: parser = argparse.ArgumentParser(description='Evaluate your report.', epilog="""Example:
To run all tests in a report: To run all tests in a report:
...@@ -109,31 +103,27 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa ...@@ -109,31 +103,27 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa
show_tol_err=False, show_tol_err=False,
big_header=True): big_header=True):
from unitgrade2.version import __version__ from src.unitgrade2.version import __version__
now = datetime.now() now = datetime.now()
if big_header: if big_header:
ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom") ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom")
b = "\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] ) b = "\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] )
else: else:
b = "Unitgrade" b = "Unitgrade"
print(b + " v" + __version__)
dt_string = now.strftime("%d/%m/%Y %H:%M:%S") dt_string = now.strftime("%d/%m/%Y %H:%M:%S")
print("Started: " + dt_string) print(b + " v" + __version__ + ", started: " + dt_string+ "\n")
# print("Started: " + dt_string)
s = report.title s = report.title
if hasattr(report, "version") and report.version is not None: if hasattr(report, "version") and report.version is not None:
s += " version " + report.version s += " version " + report.version
print("Evaluating " + s, "(use --help for options)" if show_help_flag else "") print(s, "(use --help for options)" if show_help_flag else "")
# print(f"Loaded answers from: ", report.computed_answers_file, "\n") # print(f"Loaded answers from: ", report.computed_answers_file, "\n")
table_data = [] table_data = []
nL = 80
t_start = time.time() t_start = time.time()
score = {} score = {}
loader = SequentialTestLoader() loader = SequentialTestLoader()
for n, (q, w) in enumerate(report.questions): for n, (q, w) in enumerate(report.questions):
# q = q()
# q_hidden = False
# q_hidden = issubclass(q.__class__, Hidden)
if question is not None and n+1 != question: if question is not None and n+1 != question:
continue continue
suite = loader.loadTestsFromTestCase(q) suite = loader.loadTestsFromTestCase(q)
...@@ -143,12 +133,11 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa ...@@ -143,12 +133,11 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa
q.possible = 0 q.possible = 0
q.obtained = 0 q.obtained = 0
q_ = {} # Gather score in this class. q_ = {} # Gather score in this class.
from unitgrade2.unitgrade2 import UTextTestRunner from src.unitgrade2.unitgrade2 import UTextTestRunner
# unittest.Te
# q_with_outstanding_init = [item.question for item in q.items if not item.question.has_called_init_]
UTextResult.q_title_print = q_title_print # Hacky UTextResult.q_title_print = q_title_print # Hacky
UTextResult.show_progress_bar = show_progress_bar # Hacky. UTextResult.show_progress_bar = show_progress_bar # Hacky.
UTextResult.number = n UTextResult.number = n
UTextResult.nL = report.nL
res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite) res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite)
...@@ -157,20 +146,16 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa ...@@ -157,20 +146,16 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa
assert len(res.successes) + len(res.errors) + len(res.failures) == res.testsRun assert len(res.successes) + len(res.errors) + len(res.failures) == res.testsRun
# possible = int(ws @ possible)
# obtained = int(ws @ obtained)
# obtained = int(myround(int((w * obtained) / possible ))) if possible > 0 else 0
obtained = int(w * obtained * 1.0 / possible ) if possible > 0 else 0 obtained = int(w * obtained * 1.0 / possible ) if possible > 0 else 0
score[n] = {'w': w, 'possible': w, 'obtained': obtained, 'items': q_, 'title': qtitle} score[n] = {'w': w, 'possible': w, 'obtained': obtained, 'items': q_, 'title': qtitle}
q.obtained = obtained q.obtained = obtained
q.possible = possible q.possible = possible
s1 = f"*** Question q{n+1}" s1 = f" * q{n+1}) Total"
s2 = f" {q.obtained}/{w}" s2 = f" {q.obtained}/{w}"
print(s1 + ("."* (nL-len(s1)-len(s2) )) + s2 ) print(s1 + ("."* (report.nL-len(s1)-len(s2) )) + s2 )
print(" ") print(" ")
table_data.append([f"Question q{n+1}", f"{q.obtained}/{w}"]) table_data.append([f"q{n+1}) Total", f"{q.obtained}/{w}"])
ws, possible, obtained = upack(score) ws, possible, obtained = upack(score)
possible = int( msum(possible) ) possible = int( msum(possible) )
...@@ -185,10 +170,12 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa ...@@ -185,10 +170,12 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa
seconds = dt - minutes*60 seconds = dt - minutes*60
plrl = lambda i, s: str(i) + " " + s + ("s" if i != 1 else "") plrl = lambda i, s: str(i) + " " + s + ("s" if i != 1 else "")
print(f"Completed: "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +")") from src.unitgrade2.unitgrade2 import dprint
dprint(first = "Total points at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +")",
last=""+str(report.obtained)+"/"+str(report.possible), nL = report.nL)
# print(f"Completed at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +"). Total")
table_data.append(["Total", ""+str(report.obtained)+"/"+str(report.possible) ]) table_data.append(["Total", ""+str(report.obtained)+"/"+str(report.possible) ])
results = {'total': (obtained, possible), 'details': score} results = {'total': (obtained, possible), 'details': score}
return results, table_data return results, table_data
__version__ = "0.0.2"
\ No newline at end of file
Metadata-Version: 1.0
Name: unitgrade
Version: 0.0.5
Summary: A lightweight student evaluation framework build on unittest
Home-page: https://lab.compute.dtu.dk/tuhe/unitgrade
Author: Tue Herlau
Author-email: tuhe@dtu.dk
License: Apache
Description: UNKNOWN
Platform: UNKNOWN
MANIFEST.in
README.md
setup.py
cs101courseware_example/Report0_resources_do_not_hand_in.dat
cs101courseware_example/Report1_resources_do_not_hand_in.dat
cs101courseware_example/Report2_resources_do_not_hand_in.dat
cs101courseware_example/__init__.py
cs101courseware_example/cs101report1.py
cs101courseware_example/cs101report1_grade.py
cs101courseware_example/cs101report2.py
cs101courseware_example/cs101report2_grade.py
cs101courseware_example/homework1.py
cs101courseware_example/instructions.py
unitgrade/__init__.py
unitgrade/unitgrade.py
unitgrade/unitgrade_helpers.py
unitgrade.egg-info/PKG-INFO
unitgrade.egg-info/SOURCES.txt
unitgrade.egg-info/dependency_links.txt
unitgrade.egg-info/requires.txt
unitgrade.egg-info/top_level.txt
\ No newline at end of file
jinja2
tabulate
sklearn
compress_pickle
cs101courseware_example
unitgrade
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment