diff --git a/README.md b/README.md index 87cc7ab3e2ccc93854bf87e4fa0a634fba2883f5..460eea55cb2d14a0986eb73c86c36613d8b3c4c9 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ # Unitgrade -Easy-to-use, easy-to-extend report evaluations in a framework building on pythons unittest classes - -- 100% Python -- Easy to use for students: - - Run tests as a single python command - - See your score immediately - - Upload your results as a single file on campusnet with no risk of accidential tampering - - All tests are simple classes so it will integrates well with debugger and any IDE -- No need to write external configuration or test files manually +Unitgrade is an automatic report and exam evaluation framework that enables instructors to offer automatically evaluated programming assignments. + 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 + - No external configuration files: Just write a `unittest` + - No unnatural limitations: Use any package or framework. If you can `unittest` it, it works. + - Granular security model: + - Students get public `unittests` for easy development of solutions + - 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 + - Tests are quick to run and will integrate with your IDE ## Installation Unitgrade can be installed through pip using @@ -91,7 +93,7 @@ This runs the same tests, and generates a file `Report0_handin_18_of_18.token`. ### 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. -## FAQ +# FAQ - **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. diff --git a/cs101courseware_example/Report1.xlsx b/cs101courseware_example/Report1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..21cfd1b5179c90c500320a49c6fb3b36505d9c34 Binary files /dev/null and b/cs101courseware_example/Report1.xlsx differ diff --git a/cs101courseware_example/Report1_resources_do_not_hand_in.dat b/cs101courseware_example/Report1_resources_do_not_hand_in.dat index 4ee120f4b7d596130d45560d99302cdbf38c443c..80cb22ccea7f089093d2a4ff0e75a55cd503da28 100644 Binary files a/cs101courseware_example/Report1_resources_do_not_hand_in.dat and b/cs101courseware_example/Report1_resources_do_not_hand_in.dat differ diff --git a/cs101courseware_example/Report2_resources_do_not_hand_in.dat b/cs101courseware_example/Report2_resources_do_not_hand_in.dat index 0a25a03f609ec47996f008fd7406f43b639cc74c..7902cb7632e81722996eca2621b4f9da3f7d8b0e 100644 Binary files a/cs101courseware_example/Report2_resources_do_not_hand_in.dat and b/cs101courseware_example/Report2_resources_do_not_hand_in.dat differ diff --git a/cs101courseware_example/__pycache__/__init__.cpython-38.pyc b/cs101courseware_example/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4b9f0b3284b408c2767cd2e0f124b2b7a70cb86 Binary files /dev/null and b/cs101courseware_example/__pycache__/__init__.cpython-38.pyc differ diff --git a/cs101courseware_example/__pycache__/homework1.cpython-38.pyc b/cs101courseware_example/__pycache__/homework1.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..674ec1107c08d6922af8e6e3b30d52d30cb68107 Binary files /dev/null and b/cs101courseware_example/__pycache__/homework1.cpython-38.pyc differ diff --git a/cs101courseware_example/cs101report1_grade.py b/cs101courseware_example/cs101report1_grade.py index 72e72b55ee09abc5317b9e63945326aa3fdcd0d6..15cdc7ec6d1cac749f3efb146c316dc4db3e108f 100644 --- a/cs101courseware_example/cs101report1_grade.py +++ b/cs101courseware_example/cs101report1_grade.py @@ -1,36 +1,49 @@ '''WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt.''' import numpy as np -fsQIGnDNmJuSbCgMlxcj=str -fsQIGnDNmJuSbCgMlxcd=None -fsQIGnDNmJuSbCgMlxci=int -fsQIGnDNmJuSbCgMlxcH=False -fsQIGnDNmJuSbCgMlxca=print -fsQIGnDNmJuSbCgMlxcW=len -fsQIGnDNmJuSbCgMlxco=enumerate -fsQIGnDNmJuSbCgMlxcw=issubclass -fsQIGnDNmJuSbCgMlxcB=getattr -fsQIGnDNmJuSbCgMlxch=open -fsQIGnDNmJuSbCgMlxcP=eval -fsQIGnDNmJuSbCgMlxcr=globals -fsQIGnDNmJuSbCgMlxcy=True -fsQIGnDNmJuSbCgMlxck=np.asarray +iFlPxdyCqBrgMNDzVocS=str +iFlPxdyCqBrgMNDzVocQ=None +iFlPxdyCqBrgMNDzVocY=False +iFlPxdyCqBrgMNDzVocT=int +iFlPxdyCqBrgMNDzVock=Exception +iFlPxdyCqBrgMNDzVoct=enumerate +iFlPxdyCqBrgMNDzVocs=hasattr +iFlPxdyCqBrgMNDzVocL=dict +iFlPxdyCqBrgMNDzVocA=range +iFlPxdyCqBrgMNDzVocu=list +iFlPxdyCqBrgMNDzVocR=print +iFlPxdyCqBrgMNDzVocG=True +iFlPxdyCqBrgMNDzVocO=len +iFlPxdyCqBrgMNDzVocI=issubclass +iFlPxdyCqBrgMNDzVocW=eval +iFlPxdyCqBrgMNDzVoce=max +iFlPxdyCqBrgMNDzVocH=getattr +iFlPxdyCqBrgMNDzVocX=open +iFlPxdyCqBrgMNDzVojf=globals +iFlPxdyCqBrgMNDzVojE=bytes +iFlPxdyCqBrgMNDzVocf=np.round +iFlPxdyCqBrgMNDzVoEX=np.asarray from tabulate import tabulate from datetime import datetime -fsQIGnDNmJuSbCgMlxcq=datetime.now -import pickle -fsQIGnDNmJuSbCgMlxct=pickle.loads +iFlPxdyCqBrgMNDzVocE=datetime.now import pyfiglet -fsQIGnDNmJuSbCgMlxcp=pyfiglet.figlet_format +iFlPxdyCqBrgMNDzVocj=pyfiglet.figlet_format import inspect -fsQIGnDNmJuSbCgMlxcF=inspect.currentframe -fsQIGnDNmJuSbCgMlxcT=inspect.getouterframes +iFlPxdyCqBrgMNDzVocK=inspect.currentframe +iFlPxdyCqBrgMNDzVocb=inspect.getouterframes import os -fsQIGnDNmJuSbCgMlxcX=os.getcwd -fsQIGnDNmJuSbCgMlxcK=os.path +iFlPxdyCqBrgMNDzVocn=os.getcwd +iFlPxdyCqBrgMNDzVocv=os.walk +iFlPxdyCqBrgMNDzVocw=os.path import argparse -fsQIGnDNmJuSbCgMlxcV=argparse.RawTextHelpFormatter -fsQIGnDNmJuSbCgMlxcz=argparse.ArgumentParser -fsQIGnDNmJuSbCgMlxUc=fsQIGnDNmJuSbCgMlxcz(description='Evaluate your report.',epilog="""Example: +iFlPxdyCqBrgMNDzVocp=argparse.RawTextHelpFormatter +iFlPxdyCqBrgMNDzVoca=argparse.ArgumentParser +import sys +iFlPxdyCqBrgMNDzVocJ=sys.stdout +import time +iFlPxdyCqBrgMNDzVocm=time.time +import threading +import tqdm +iFlPxdyCqBrgMNDzVofE=iFlPxdyCqBrgMNDzVoca(description='Evaluate your report.',epilog="""Example: To run all tests in a report: > python assignment1_dp.py To run only question 2 or question 2.1 @@ -39,151 +52,269 @@ To run only question 2 or question 2.1 Note this scripts does not grade your report. To grade your report, use: > python report1_grade.py Finally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful. -As an old, suppose the report file is Documents/course_package/assignment1_dp.py, and `course_package` is a python package. Change directory to 'Documents/` and execute: +For instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to 'Documents/` and run: > python -m course_package.report1 see https://docs.python.org/3.9/using/cmdline.html -""", formatter_class=fsQIGnDNmJuSbCgMlxcV) -fsQIGnDNmJuSbCgMlxUc.add_argument('-q',nargs='?',type=fsQIGnDNmJuSbCgMlxcj,default=fsQIGnDNmJuSbCgMlxcd,help='Only evaluate this question (old: -q 2)') -fsQIGnDNmJuSbCgMlxUc.add_argument('--showexpected',action="store_true",help='Show the expected/desired result') -fsQIGnDNmJuSbCgMlxUc.add_argument('--showcomputed',action="store_true",help='Show the answer your code computes') -fsQIGnDNmJuSbCgMlxUc.add_argument('--unmute',action="store_true",help='Show result of print(...) commands in code') -fsQIGnDNmJuSbCgMlxUc.add_argument('--passall',action="store_true",help='Automatically pass all tests. Useful when debugging.') -def fsQIGnDNmJuSbCgMlxUE(fsQIGnDNmJuSbCgMlxUo,question=fsQIGnDNmJuSbCgMlxcd,qitem=fsQIGnDNmJuSbCgMlxcd): - fsQIGnDNmJuSbCgMlxUq=fsQIGnDNmJuSbCgMlxUc.parse_args() - if question is fsQIGnDNmJuSbCgMlxcd and fsQIGnDNmJuSbCgMlxUq.q is not fsQIGnDNmJuSbCgMlxcd: - question=fsQIGnDNmJuSbCgMlxUq.q +""", formatter_class=iFlPxdyCqBrgMNDzVocp) +iFlPxdyCqBrgMNDzVofE.add_argument('-q',nargs='?',type=iFlPxdyCqBrgMNDzVocS,default=iFlPxdyCqBrgMNDzVocQ,help='Only evaluate this question (e.g.: -q 2)') +iFlPxdyCqBrgMNDzVofE.add_argument('--showexpected',action="store_true",help='Show the expected/desired result') +iFlPxdyCqBrgMNDzVofE.add_argument('--showcomputed',action="store_true",help='Show the answer your code computes') +iFlPxdyCqBrgMNDzVofE.add_argument('--unmute',action="store_true",help='Show result of print(...) commands in code') +iFlPxdyCqBrgMNDzVofE.add_argument('--passall',action="store_true",help='Automatically pass all tests. Useful when debugging.') +def iFlPxdyCqBrgMNDzVoER(iFlPxdyCqBrgMNDzVofu,question=iFlPxdyCqBrgMNDzVocQ,qitem=iFlPxdyCqBrgMNDzVocQ,unmute=iFlPxdyCqBrgMNDzVocQ,passall=iFlPxdyCqBrgMNDzVocQ,ignore_missing_file=iFlPxdyCqBrgMNDzVocY,show_tol_err=iFlPxdyCqBrgMNDzVocY): + iFlPxdyCqBrgMNDzVofb=iFlPxdyCqBrgMNDzVofE.parse_args() + if question is iFlPxdyCqBrgMNDzVocQ and iFlPxdyCqBrgMNDzVofb.q is not iFlPxdyCqBrgMNDzVocQ: + question=iFlPxdyCqBrgMNDzVofb.q if "." in question: - question,qitem=[fsQIGnDNmJuSbCgMlxci(v)for v in question.split(".")] + question,qitem=[iFlPxdyCqBrgMNDzVocT(v)for v in question.split(".")] + else: + question=iFlPxdyCqBrgMNDzVocT(question) + if not iFlPxdyCqBrgMNDzVocw.isfile(iFlPxdyCqBrgMNDzVofu.computed_answers_file)and not ignore_missing_file: + raise iFlPxdyCqBrgMNDzVock("> Error: The pre-computed answer file",iFlPxdyCqBrgMNDzVocw.abspath(iFlPxdyCqBrgMNDzVofu.computed_answers_file),"does not exist. Check your package installation") + if unmute is iFlPxdyCqBrgMNDzVocQ: + unmute=iFlPxdyCqBrgMNDzVofb.unmute + if passall is iFlPxdyCqBrgMNDzVocQ: + passall=iFlPxdyCqBrgMNDzVofb.passall + iFlPxdyCqBrgMNDzVofa,iFlPxdyCqBrgMNDzVofp=iFlPxdyCqBrgMNDzVoEO(iFlPxdyCqBrgMNDzVofu,question=question,show_progress_bar=not unmute,qitem=qitem,verbose=iFlPxdyCqBrgMNDzVocY,passall=passall,show_expected=iFlPxdyCqBrgMNDzVofb.showexpected,show_computed=iFlPxdyCqBrgMNDzVofb.showcomputed,unmute=unmute,show_tol_err=show_tol_err) + try: + import irlc.lectures + import xlwings + from openpyxl import Workbook + import pandas as pd + from collections import defaultdict + dd=defaultdict(lambda:[]) + iFlPxdyCqBrgMNDzVofJ=[] + for k1,(q,_)in iFlPxdyCqBrgMNDzVoct(iFlPxdyCqBrgMNDzVofu.questions): + for k2,(iFlPxdyCqBrgMNDzVofX,_)in iFlPxdyCqBrgMNDzVoct(q.items): + dd['question_index'].append(k1) + dd['item_index'].append(k2) + dd['question'].append(q.name) + dd['item'].append(iFlPxdyCqBrgMNDzVofX.name) + dd['tol'].append(0 if not iFlPxdyCqBrgMNDzVocs(iFlPxdyCqBrgMNDzVofX,'tol')else iFlPxdyCqBrgMNDzVofX.tol) + iFlPxdyCqBrgMNDzVofJ.append(0 if not iFlPxdyCqBrgMNDzVocs(iFlPxdyCqBrgMNDzVofX,'error_computed')else iFlPxdyCqBrgMNDzVofX.error_computed) + iFlPxdyCqBrgMNDzVofm=iFlPxdyCqBrgMNDzVofu.wdir+"/"+iFlPxdyCqBrgMNDzVofu.name+".xlsx" + if iFlPxdyCqBrgMNDzVocw.isfile(iFlPxdyCqBrgMNDzVofm): + iFlPxdyCqBrgMNDzVofU=pd.read_excel(iFlPxdyCqBrgMNDzVofm).to_dict() else: - question=fsQIGnDNmJuSbCgMlxci(question) - fsQIGnDNmJuSbCgMlxUT,fsQIGnDNmJuSbCgMlxUF=fsQIGnDNmJuSbCgMlxUO(fsQIGnDNmJuSbCgMlxUo,question=question,qitem=qitem,verbose=fsQIGnDNmJuSbCgMlxcH,passall=fsQIGnDNmJuSbCgMlxUq.passall,show_expected=fsQIGnDNmJuSbCgMlxUq.showexpected,show_computed=fsQIGnDNmJuSbCgMlxUq.showcomputed,unmute=fsQIGnDNmJuSbCgMlxUq.unmute) - if question is fsQIGnDNmJuSbCgMlxcd: - fsQIGnDNmJuSbCgMlxca("Provisional evaluation") - tabulate(fsQIGnDNmJuSbCgMlxUF) - fsQIGnDNmJuSbCgMlxUK=fsQIGnDNmJuSbCgMlxUF - fsQIGnDNmJuSbCgMlxca(tabulate(fsQIGnDNmJuSbCgMlxUK)) - fsQIGnDNmJuSbCgMlxca(" ") - fsQIGnDNmJuSbCgMlxca("Note your results have not yet been registered. \nTo register your results, please run the file:") - fr=fsQIGnDNmJuSbCgMlxcT(fsQIGnDNmJuSbCgMlxcF())[1].filename - fsQIGnDNmJuSbCgMlxca(">>>",fsQIGnDNmJuSbCgMlxcK.basename(fr)[:-3]+"_grade.py") - fsQIGnDNmJuSbCgMlxca("In the same manner as you ran this file.") - return fsQIGnDNmJuSbCgMlxUT -def fsQIGnDNmJuSbCgMlxUe(q): + iFlPxdyCqBrgMNDzVofU=iFlPxdyCqBrgMNDzVocL() + for k in iFlPxdyCqBrgMNDzVocA(1000): + iFlPxdyCqBrgMNDzVofh='run_'+iFlPxdyCqBrgMNDzVocS(k) + if iFlPxdyCqBrgMNDzVofh in iFlPxdyCqBrgMNDzVofU: + dd[iFlPxdyCqBrgMNDzVofh]=iFlPxdyCqBrgMNDzVocu(iFlPxdyCqBrgMNDzVofU['run_0'].values()) + else: + dd[iFlPxdyCqBrgMNDzVofh]=iFlPxdyCqBrgMNDzVofJ + break + iFlPxdyCqBrgMNDzVofS=Workbook() + iFlPxdyCqBrgMNDzVofQ=iFlPxdyCqBrgMNDzVofS.active + for iFlPxdyCqBrgMNDzVofY,iFlPxdyCqBrgMNDzVofh in iFlPxdyCqBrgMNDzVoct(dd.keys()): + iFlPxdyCqBrgMNDzVofQ.cell(row=1,column=iFlPxdyCqBrgMNDzVofY+1).value=iFlPxdyCqBrgMNDzVofh + for iFlPxdyCqBrgMNDzVofT,iFlPxdyCqBrgMNDzVofX in iFlPxdyCqBrgMNDzVoct(dd[iFlPxdyCqBrgMNDzVofh]): + iFlPxdyCqBrgMNDzVofQ.cell(row=iFlPxdyCqBrgMNDzVofT+2,column=iFlPxdyCqBrgMNDzVofY+1).value=iFlPxdyCqBrgMNDzVofX + iFlPxdyCqBrgMNDzVofS.save(iFlPxdyCqBrgMNDzVofm) + iFlPxdyCqBrgMNDzVofS.close() + except ModuleNotFoundError as e: + s=234 + pass + if question is iFlPxdyCqBrgMNDzVocQ: + iFlPxdyCqBrgMNDzVocR("Provisional evaluation") + tabulate(iFlPxdyCqBrgMNDzVofp) + iFlPxdyCqBrgMNDzVofk=iFlPxdyCqBrgMNDzVofp + iFlPxdyCqBrgMNDzVocR(tabulate(iFlPxdyCqBrgMNDzVofk)) + iFlPxdyCqBrgMNDzVocR(" ") + fr=iFlPxdyCqBrgMNDzVocb(iFlPxdyCqBrgMNDzVocK())[1].filename + iFlPxdyCqBrgMNDzVoft=iFlPxdyCqBrgMNDzVocw.basename(fr)[:-3]+"_grade.py" + if iFlPxdyCqBrgMNDzVocw.exists(iFlPxdyCqBrgMNDzVoft): + iFlPxdyCqBrgMNDzVocR("Note your results have not yet been registered. \nTo register your results, please run the file:") + iFlPxdyCqBrgMNDzVocR(">>>",iFlPxdyCqBrgMNDzVoft) + iFlPxdyCqBrgMNDzVocR("In the same manner as you ran this file.") + return iFlPxdyCqBrgMNDzVofa +def iFlPxdyCqBrgMNDzVoEG(q): h=[(i['w'],i['possible'],i['obtained'])for i in q.values()] - h=fsQIGnDNmJuSbCgMlxck(h) + h=iFlPxdyCqBrgMNDzVoEX(h) return h[:,0],h[:,1],h[:,2], -def fsQIGnDNmJuSbCgMlxUO(fsQIGnDNmJuSbCgMlxUo,question=fsQIGnDNmJuSbCgMlxcd,qitem=fsQIGnDNmJuSbCgMlxcd,passall=fsQIGnDNmJuSbCgMlxcH,verbose=fsQIGnDNmJuSbCgMlxcH,show_expected=fsQIGnDNmJuSbCgMlxcH,show_computed=fsQIGnDNmJuSbCgMlxcH,unmute=fsQIGnDNmJuSbCgMlxcH): - fsQIGnDNmJuSbCgMlxUX=fsQIGnDNmJuSbCgMlxcq() - fsQIGnDNmJuSbCgMlxUz=fsQIGnDNmJuSbCgMlxcp("UnitGrade",font="doom") - b="\n".join([l for l in fsQIGnDNmJuSbCgMlxUz.splitlines()if fsQIGnDNmJuSbCgMlxcW(l.strip())>0]) - fsQIGnDNmJuSbCgMlxca(b+" v"+__version__) - fsQIGnDNmJuSbCgMlxUV=fsQIGnDNmJuSbCgMlxUX.strftime("%d/%m/%Y %H:%M:%S") - fsQIGnDNmJuSbCgMlxca("Started: "+fsQIGnDNmJuSbCgMlxUV) - fsQIGnDNmJuSbCgMlxca("Evaluating "+fsQIGnDNmJuSbCgMlxUo.title,"(use --help for options)") - fsQIGnDNmJuSbCgMlxca(f"Loaded answers from: ",fsQIGnDNmJuSbCgMlxUo.computed_answers_file,"\n") - fsQIGnDNmJuSbCgMlxUF=[] +def iFlPxdyCqBrgMNDzVoEO(iFlPxdyCqBrgMNDzVofu,question=iFlPxdyCqBrgMNDzVocQ,qitem=iFlPxdyCqBrgMNDzVocQ,passall=iFlPxdyCqBrgMNDzVocY,verbose=iFlPxdyCqBrgMNDzVocY,show_expected=iFlPxdyCqBrgMNDzVocY,show_computed=iFlPxdyCqBrgMNDzVocY,unmute=iFlPxdyCqBrgMNDzVocY,show_help_flag=iFlPxdyCqBrgMNDzVocG,silent=iFlPxdyCqBrgMNDzVocY,show_progress_bar=iFlPxdyCqBrgMNDzVocG,show_tol_err=iFlPxdyCqBrgMNDzVocY): + iFlPxdyCqBrgMNDzVofs=iFlPxdyCqBrgMNDzVocE() + iFlPxdyCqBrgMNDzVofL=iFlPxdyCqBrgMNDzVocj("UnitGrade",font="doom") + b="\n".join([l for l in iFlPxdyCqBrgMNDzVofL.splitlines()if iFlPxdyCqBrgMNDzVocO(l.strip())>0]) + iFlPxdyCqBrgMNDzVocR(b+" v"+__version__) + iFlPxdyCqBrgMNDzVofA=iFlPxdyCqBrgMNDzVofs.strftime("%d/%m/%Y %H:%M:%S") + iFlPxdyCqBrgMNDzVocR("Started: "+iFlPxdyCqBrgMNDzVofA) + s=iFlPxdyCqBrgMNDzVofu.title + if iFlPxdyCqBrgMNDzVofu.version is not iFlPxdyCqBrgMNDzVocQ: + s+=" version "+iFlPxdyCqBrgMNDzVofu.version + iFlPxdyCqBrgMNDzVocR("Evaluating "+s,"(use --help for options)" if show_help_flag else "") + iFlPxdyCqBrgMNDzVocR(f"Loaded answers from: ",iFlPxdyCqBrgMNDzVofu.computed_answers_file,"\n") + iFlPxdyCqBrgMNDzVofp=[] nL=80 - fsQIGnDNmJuSbCgMlxUY={} - for n,(q,w)in fsQIGnDNmJuSbCgMlxco(fsQIGnDNmJuSbCgMlxUo.questions): - fsQIGnDNmJuSbCgMlxUj=fsQIGnDNmJuSbCgMlxcw(q.__class__,Hidden) - if question is not fsQIGnDNmJuSbCgMlxcd and n+1!=question: + iFlPxdyCqBrgMNDzVofR=iFlPxdyCqBrgMNDzVocm() + iFlPxdyCqBrgMNDzVofG={} + for n,(q,w)in iFlPxdyCqBrgMNDzVoct(iFlPxdyCqBrgMNDzVofu.questions): + iFlPxdyCqBrgMNDzVofO=iFlPxdyCqBrgMNDzVocI(q.__class__,Hidden) + if question is not iFlPxdyCqBrgMNDzVocQ and n+1!=question: continue - fsQIGnDNmJuSbCgMlxca(f"Question {n+1}: {q.title}") - fsQIGnDNmJuSbCgMlxca("="*nL) + iFlPxdyCqBrgMNDzVofI="Question %i: %s"%(n+1,q.title) + iFlPxdyCqBrgMNDzVocR(iFlPxdyCqBrgMNDzVofI,end="") q.possible=0 q.obtained=0 q_={} - for j,(item,iw)in fsQIGnDNmJuSbCgMlxco(q.items): - if qitem is not fsQIGnDNmJuSbCgMlxcd and question is not fsQIGnDNmJuSbCgMlxcd and item is not fsQIGnDNmJuSbCgMlxcd and j+1!=qitem: + for j,(iFlPxdyCqBrgMNDzVofX,iw)in iFlPxdyCqBrgMNDzVoct(q.items): + if qitem is not iFlPxdyCqBrgMNDzVocQ and question is not iFlPxdyCqBrgMNDzVocQ and iFlPxdyCqBrgMNDzVofX is not iFlPxdyCqBrgMNDzVocQ and j+1!=qitem: continue - ss=f"*** q{n+1}.{j+1}) {item.title}" - el=nL-4 - if fsQIGnDNmJuSbCgMlxcW(ss)<el: - ss+='.'*(el-fsQIGnDNmJuSbCgMlxcW(ss)) - fsQIGnDNmJuSbCgMlxUd=fsQIGnDNmJuSbCgMlxcw(item.__class__,Hidden) - if not fsQIGnDNmJuSbCgMlxUd: - fsQIGnDNmJuSbCgMlxca(ss,end="") - (fsQIGnDNmJuSbCgMlxUH,fsQIGnDNmJuSbCgMlxUa)=item.get_points(show_expected=show_expected,show_computed=show_computed,unmute=unmute,passall=passall) - q_[j]={'w':iw,'possible':fsQIGnDNmJuSbCgMlxUa,'obtained':fsQIGnDNmJuSbCgMlxUH,'hidden':fsQIGnDNmJuSbCgMlxUd} - if not fsQIGnDNmJuSbCgMlxUd: - if fsQIGnDNmJuSbCgMlxUH==fsQIGnDNmJuSbCgMlxUa: - fsQIGnDNmJuSbCgMlxca(f"PASS") - else: - fsQIGnDNmJuSbCgMlxca(f"*** FAILED") - ws,fsQIGnDNmJuSbCgMlxUa,fsQIGnDNmJuSbCgMlxUW=fsQIGnDNmJuSbCgMlxUe(q_) - fsQIGnDNmJuSbCgMlxUa=fsQIGnDNmJuSbCgMlxci(ws@fsQIGnDNmJuSbCgMlxUa) - fsQIGnDNmJuSbCgMlxUW=fsQIGnDNmJuSbCgMlxci(ws@fsQIGnDNmJuSbCgMlxUW) - fsQIGnDNmJuSbCgMlxUW=fsQIGnDNmJuSbCgMlxci(myround(fsQIGnDNmJuSbCgMlxci((w*fsQIGnDNmJuSbCgMlxUW)/fsQIGnDNmJuSbCgMlxUa)))if fsQIGnDNmJuSbCgMlxUa>0 else 0 - fsQIGnDNmJuSbCgMlxUY[n]={'w':w,'possible':w,'obtained':fsQIGnDNmJuSbCgMlxUW,'Ãtems':q_,'hidden':fsQIGnDNmJuSbCgMlxUj} - q.obtained=fsQIGnDNmJuSbCgMlxUW - q.possible=fsQIGnDNmJuSbCgMlxUa + if not q.has_called_init_: + iFlPxdyCqBrgMNDzVofe=iFlPxdyCqBrgMNDzVocm() + cc=iFlPxdyCqBrgMNDzVocQ + if show_progress_bar: + cc=ActiveProgress(t=q.estimated_time,title=iFlPxdyCqBrgMNDzVofI) + with iFlPxdyCqBrgMNDzVocW('Capturing')(unmute=unmute): + try: + q.init() + except iFlPxdyCqBrgMNDzVock as e: + if not passall: + if not silent: + iFlPxdyCqBrgMNDzVocR(" ") + iFlPxdyCqBrgMNDzVocR("="*30) + iFlPxdyCqBrgMNDzVocR(f"When initializing question {q.title} the initialization code threw an error") + iFlPxdyCqBrgMNDzVocR(e) + iFlPxdyCqBrgMNDzVocR("The remaining parts of this question will likely fail.") + iFlPxdyCqBrgMNDzVocR("="*30) + if show_progress_bar: + cc.terminate() + iFlPxdyCqBrgMNDzVocJ.flush() + iFlPxdyCqBrgMNDzVocR(iFlPxdyCqBrgMNDzVofI,end="") + q.has_called_init_=iFlPxdyCqBrgMNDzVocG + iFlPxdyCqBrgMNDzVofH=iFlPxdyCqBrgMNDzVocf(iFlPxdyCqBrgMNDzVocm()-iFlPxdyCqBrgMNDzVofe,2) + iFlPxdyCqBrgMNDzVocR(" "*iFlPxdyCqBrgMNDzVoce(0,nL-iFlPxdyCqBrgMNDzVocO(iFlPxdyCqBrgMNDzVofI))+(" ("+iFlPxdyCqBrgMNDzVocS(iFlPxdyCqBrgMNDzVofH)+" seconds)" if iFlPxdyCqBrgMNDzVofH>=0.1 else "")) + iFlPxdyCqBrgMNDzVocR("="*nL) + iFlPxdyCqBrgMNDzVofX.question=q + iFlPxdyCqBrgMNDzVoEf=ss="*** q%i.%i) %s"%(n+1,j+1,iFlPxdyCqBrgMNDzVofX.title) + if show_progress_bar: + cc=ActiveProgress(t=iFlPxdyCqBrgMNDzVofX.estimated_time,title=iFlPxdyCqBrgMNDzVoEf) + else: + iFlPxdyCqBrgMNDzVocR(iFlPxdyCqBrgMNDzVoEf+('.'*iFlPxdyCqBrgMNDzVoce(0,nL-4-iFlPxdyCqBrgMNDzVocO(ss))),end="") + iFlPxdyCqBrgMNDzVoEc=iFlPxdyCqBrgMNDzVocI(iFlPxdyCqBrgMNDzVofX.__class__,Hidden) + iFlPxdyCqBrgMNDzVofe=iFlPxdyCqBrgMNDzVocm() + (iFlPxdyCqBrgMNDzVoEj,iFlPxdyCqBrgMNDzVoEb)=iFlPxdyCqBrgMNDzVofX.get_points(show_expected=show_expected,show_computed=show_computed,unmute=unmute,passall=passall,silent=silent) + q_[j]={'w':iw,'possible':iFlPxdyCqBrgMNDzVoEb,'obtained':iFlPxdyCqBrgMNDzVoEj,'hidden':iFlPxdyCqBrgMNDzVoEc,'computed':iFlPxdyCqBrgMNDzVocS(iFlPxdyCqBrgMNDzVofX._computed_answer),'title':iFlPxdyCqBrgMNDzVofX.title} + iFlPxdyCqBrgMNDzVoEK=iFlPxdyCqBrgMNDzVocf(iFlPxdyCqBrgMNDzVocm()-iFlPxdyCqBrgMNDzVofe,2) + if show_progress_bar: + cc.terminate() + iFlPxdyCqBrgMNDzVocJ.flush() + iFlPxdyCqBrgMNDzVocR(iFlPxdyCqBrgMNDzVoEf+('.'*iFlPxdyCqBrgMNDzVoce(0,nL-4-iFlPxdyCqBrgMNDzVocO(ss))),end="") + if not iFlPxdyCqBrgMNDzVoEc: + ss="PASS" if iFlPxdyCqBrgMNDzVoEj==iFlPxdyCqBrgMNDzVoEb else "*** FAILED" + if iFlPxdyCqBrgMNDzVoEK>=0.1: + ss+=" ("+iFlPxdyCqBrgMNDzVocS(iFlPxdyCqBrgMNDzVoEK)+" seconds)" + iFlPxdyCqBrgMNDzVocR(ss) + ws,iFlPxdyCqBrgMNDzVoEb,iFlPxdyCqBrgMNDzVoEw=iFlPxdyCqBrgMNDzVoEG(q_) + iFlPxdyCqBrgMNDzVoEb=iFlPxdyCqBrgMNDzVocT(ws@iFlPxdyCqBrgMNDzVoEb) + iFlPxdyCqBrgMNDzVoEw=iFlPxdyCqBrgMNDzVocT(ws@iFlPxdyCqBrgMNDzVoEw) + iFlPxdyCqBrgMNDzVoEw=iFlPxdyCqBrgMNDzVocT(myround(iFlPxdyCqBrgMNDzVocT((w*iFlPxdyCqBrgMNDzVoEw)/iFlPxdyCqBrgMNDzVoEb)))if iFlPxdyCqBrgMNDzVoEb>0 else 0 + iFlPxdyCqBrgMNDzVofG[n]={'w':w,'possible':w,'obtained':iFlPxdyCqBrgMNDzVoEw,'items':q_,'hidden':iFlPxdyCqBrgMNDzVofO,'title':q.title} + q.obtained=iFlPxdyCqBrgMNDzVoEw + q.possible=iFlPxdyCqBrgMNDzVoEb s1=f"*** Question q{n+1}" s2=f" {q.obtained}/{w}" - fsQIGnDNmJuSbCgMlxca(s1+("."*(nL-fsQIGnDNmJuSbCgMlxcW(s1)-fsQIGnDNmJuSbCgMlxcW(s2)))+s2) - fsQIGnDNmJuSbCgMlxca(" ") - fsQIGnDNmJuSbCgMlxUF.append([f"Question q{n+1}",f"{q.obtained}/{w}"]) - ws,fsQIGnDNmJuSbCgMlxUa,fsQIGnDNmJuSbCgMlxUW=fsQIGnDNmJuSbCgMlxUe(fsQIGnDNmJuSbCgMlxUY) - fsQIGnDNmJuSbCgMlxUa=fsQIGnDNmJuSbCgMlxci(msum(fsQIGnDNmJuSbCgMlxUa)) - fsQIGnDNmJuSbCgMlxUW=fsQIGnDNmJuSbCgMlxci(msum(fsQIGnDNmJuSbCgMlxUW)) - fsQIGnDNmJuSbCgMlxUo.possible=fsQIGnDNmJuSbCgMlxUa - fsQIGnDNmJuSbCgMlxUo.obtained=fsQIGnDNmJuSbCgMlxUW - fsQIGnDNmJuSbCgMlxUX=fsQIGnDNmJuSbCgMlxcq() - fsQIGnDNmJuSbCgMlxUV=fsQIGnDNmJuSbCgMlxUX.strftime("%H:%M:%S") - fsQIGnDNmJuSbCgMlxca(f"Completed: "+fsQIGnDNmJuSbCgMlxUV) - fsQIGnDNmJuSbCgMlxUF.append(["Total",""+fsQIGnDNmJuSbCgMlxcj(fsQIGnDNmJuSbCgMlxUo.obtained)+"/"+fsQIGnDNmJuSbCgMlxcj(fsQIGnDNmJuSbCgMlxUo.possible)]) - fsQIGnDNmJuSbCgMlxUT={'total':(fsQIGnDNmJuSbCgMlxUW,fsQIGnDNmJuSbCgMlxUa),'details':fsQIGnDNmJuSbCgMlxUY} - return fsQIGnDNmJuSbCgMlxUT,fsQIGnDNmJuSbCgMlxUF + iFlPxdyCqBrgMNDzVocR(s1+("."*(nL-iFlPxdyCqBrgMNDzVocO(s1)-iFlPxdyCqBrgMNDzVocO(s2)))+s2) + iFlPxdyCqBrgMNDzVocR(" ") + iFlPxdyCqBrgMNDzVofp.append([f"Question q{n+1}",f"{q.obtained}/{w}"]) + ws,iFlPxdyCqBrgMNDzVoEb,iFlPxdyCqBrgMNDzVoEw=iFlPxdyCqBrgMNDzVoEG(iFlPxdyCqBrgMNDzVofG) + iFlPxdyCqBrgMNDzVoEb=iFlPxdyCqBrgMNDzVocT(msum(iFlPxdyCqBrgMNDzVoEb)) + iFlPxdyCqBrgMNDzVoEw=iFlPxdyCqBrgMNDzVocT(msum(iFlPxdyCqBrgMNDzVoEw)) + iFlPxdyCqBrgMNDzVofu.possible=iFlPxdyCqBrgMNDzVoEb + iFlPxdyCqBrgMNDzVofu.obtained=iFlPxdyCqBrgMNDzVoEw + iFlPxdyCqBrgMNDzVofs=iFlPxdyCqBrgMNDzVocE() + iFlPxdyCqBrgMNDzVofA=iFlPxdyCqBrgMNDzVofs.strftime("%H:%M:%S") + dt=iFlPxdyCqBrgMNDzVocT(iFlPxdyCqBrgMNDzVocm()-iFlPxdyCqBrgMNDzVofR) + iFlPxdyCqBrgMNDzVoEv=dt//60 + iFlPxdyCqBrgMNDzVoEn=dt-iFlPxdyCqBrgMNDzVoEv*60 + iFlPxdyCqBrgMNDzVoEa=lambda i,s:iFlPxdyCqBrgMNDzVocS(i)+" "+s+("s" if i!=1 else "") + iFlPxdyCqBrgMNDzVocR(f"Completed: "+iFlPxdyCqBrgMNDzVofA+" ("+iFlPxdyCqBrgMNDzVoEa(iFlPxdyCqBrgMNDzVoEv,"minute")+", "+iFlPxdyCqBrgMNDzVoEa(iFlPxdyCqBrgMNDzVoEn,"second")+")") + iFlPxdyCqBrgMNDzVofp.append(["Total",""+iFlPxdyCqBrgMNDzVocS(iFlPxdyCqBrgMNDzVofu.obtained)+"/"+iFlPxdyCqBrgMNDzVocS(iFlPxdyCqBrgMNDzVofu.possible)]) + iFlPxdyCqBrgMNDzVofa={'total':(iFlPxdyCqBrgMNDzVoEw,iFlPxdyCqBrgMNDzVoEb),'details':iFlPxdyCqBrgMNDzVofG} + return iFlPxdyCqBrgMNDzVofa,iFlPxdyCqBrgMNDzVofp from tabulate import tabulate from datetime import datetime -fsQIGnDNmJuSbCgMlxcq=datetime.now +iFlPxdyCqBrgMNDzVocE=datetime.now import inspect -fsQIGnDNmJuSbCgMlxcF=inspect.currentframe -fsQIGnDNmJuSbCgMlxcT=inspect.getouterframes +iFlPxdyCqBrgMNDzVocK=inspect.currentframe +iFlPxdyCqBrgMNDzVocb=inspect.getouterframes import json -fsQIGnDNmJuSbCgMlxcY=json.dumps +iFlPxdyCqBrgMNDzVocU=json.dumps import os -fsQIGnDNmJuSbCgMlxcX=os.getcwd -fsQIGnDNmJuSbCgMlxcK=os.path +iFlPxdyCqBrgMNDzVocn=os.getcwd +iFlPxdyCqBrgMNDzVocv=os.walk +iFlPxdyCqBrgMNDzVocw=os.path import bz2 import pickle -fsQIGnDNmJuSbCgMlxct=pickle.loads -def fsQIGnDNmJuSbCgMlxUA(fsQIGnDNmJuSbCgMlxUw,fsQIGnDNmJuSbCgMlxUr): - with fsQIGnDNmJuSbCgMlxcB(bz2,'open')(fsQIGnDNmJuSbCgMlxUr,"wt")as f: - f.write(fsQIGnDNmJuSbCgMlxUw) -def fsQIGnDNmJuSbCgMlxcU(fsQIGnDNmJuSbCgMlxUo,output_dir=fsQIGnDNmJuSbCgMlxcd): +iFlPxdyCqBrgMNDzVoch=pickle.loads +iFlPxdyCqBrgMNDzVocw=os.path +import os +iFlPxdyCqBrgMNDzVocn=os.getcwd +iFlPxdyCqBrgMNDzVocv=os.walk +def iFlPxdyCqBrgMNDzVoEI(iFlPxdyCqBrgMNDzVoEQ,iFlPxdyCqBrgMNDzVoEs): + with iFlPxdyCqBrgMNDzVocH(bz2,'open')(iFlPxdyCqBrgMNDzVoEs,"wt")as f: + f.write(iFlPxdyCqBrgMNDzVoEQ) +def iFlPxdyCqBrgMNDzVoEW(imp): + iFlPxdyCqBrgMNDzVoEp={} + m=imp + f=m.__file__ + iFlPxdyCqBrgMNDzVoEJ=iFlPxdyCqBrgMNDzVocw.dirname(__import__(m.__name__.split('.')[0]).__file__) + iFlPxdyCqBrgMNDzVoEJ=iFlPxdyCqBrgMNDzVocw.dirname(iFlPxdyCqBrgMNDzVoEJ) + if f.endswith("__init__.py"): + for iFlPxdyCqBrgMNDzVoEm,dirs,files in iFlPxdyCqBrgMNDzVocv(iFlPxdyCqBrgMNDzVocw.dirname(f)): + for iFlPxdyCqBrgMNDzVoEU in files: + if iFlPxdyCqBrgMNDzVoEU.endswith(".py"): + v=iFlPxdyCqBrgMNDzVocw.relpath(iFlPxdyCqBrgMNDzVocw.join(iFlPxdyCqBrgMNDzVoEm,iFlPxdyCqBrgMNDzVoEU),iFlPxdyCqBrgMNDzVoEJ) + with iFlPxdyCqBrgMNDzVocX(iFlPxdyCqBrgMNDzVocw.join(iFlPxdyCqBrgMNDzVoEm,iFlPxdyCqBrgMNDzVoEU),'r')as ff: + iFlPxdyCqBrgMNDzVoEp[v]=ff.read() + else: + v=iFlPxdyCqBrgMNDzVocw.relpath(f,iFlPxdyCqBrgMNDzVoEJ) + with iFlPxdyCqBrgMNDzVocX(f,'r')as ff: + iFlPxdyCqBrgMNDzVoEp[v]=ff.read() + return iFlPxdyCqBrgMNDzVoEp +def iFlPxdyCqBrgMNDzVoEe(iFlPxdyCqBrgMNDzVofu,output_dir=iFlPxdyCqBrgMNDzVocQ): n=80 - fsQIGnDNmJuSbCgMlxUT,fsQIGnDNmJuSbCgMlxUF=fsQIGnDNmJuSbCgMlxUO(fsQIGnDNmJuSbCgMlxUo) - fsQIGnDNmJuSbCgMlxca(" ") - fsQIGnDNmJuSbCgMlxca("="*n) - fsQIGnDNmJuSbCgMlxca("Final evaluation") - fsQIGnDNmJuSbCgMlxca(tabulate(fsQIGnDNmJuSbCgMlxUF)) - fsQIGnDNmJuSbCgMlxUT['sources']={} - fsQIGnDNmJuSbCgMlxca("Gathering files...") - for m in fsQIGnDNmJuSbCgMlxUo.pack_imports: - with fsQIGnDNmJuSbCgMlxch(m.__file__,'r')as f: - fsQIGnDNmJuSbCgMlxUT['sources'][m.__name__]=f.read() - fsQIGnDNmJuSbCgMlxca(f"*** {m.__name__}") - fsQIGnDNmJuSbCgMlxUT['sources']={} - fsQIGnDNmJuSbCgMlxUw=fsQIGnDNmJuSbCgMlxcY(fsQIGnDNmJuSbCgMlxUT,indent=4) - if output_dir is fsQIGnDNmJuSbCgMlxcd: - output_dir=fsQIGnDNmJuSbCgMlxcX() - fsQIGnDNmJuSbCgMlxUh=fsQIGnDNmJuSbCgMlxUo.__class__.__name__+"_handin" - fsQIGnDNmJuSbCgMlxUP,fsQIGnDNmJuSbCgMlxUa=fsQIGnDNmJuSbCgMlxUT['total'] - fsQIGnDNmJuSbCgMlxUr="%s_%i_of_%i.token"%(fsQIGnDNmJuSbCgMlxUh,fsQIGnDNmJuSbCgMlxUP,fsQIGnDNmJuSbCgMlxUa) - fsQIGnDNmJuSbCgMlxUr=fsQIGnDNmJuSbCgMlxcK.join(output_dir,fsQIGnDNmJuSbCgMlxUr) - fsQIGnDNmJuSbCgMlxUA(fsQIGnDNmJuSbCgMlxUw,fsQIGnDNmJuSbCgMlxUr) - fsQIGnDNmJuSbCgMlxca(" ") - fsQIGnDNmJuSbCgMlxca("To get credit for your results, please upload the single file: ") - fsQIGnDNmJuSbCgMlxca(">",fsQIGnDNmJuSbCgMlxUr) - fsQIGnDNmJuSbCgMlxca("To campusnet without any modifications.") -def fsQIGnDNmJuSbCgMlxcR(fsQIGnDNmJuSbCgMlxUv,fsQIGnDNmJuSbCgMlxUy,payload): - fsQIGnDNmJuSbCgMlxcP("exec")(fsQIGnDNmJuSbCgMlxUy,fsQIGnDNmJuSbCgMlxcr()) - pl=fsQIGnDNmJuSbCgMlxct(payload) - fsQIGnDNmJuSbCgMlxUo=fsQIGnDNmJuSbCgMlxcP(fsQIGnDNmJuSbCgMlxUv)(payload=pl,strict=fsQIGnDNmJuSbCgMlxcy) - return fsQIGnDNmJuSbCgMlxUo -fsQIGnDNmJuSbCgMlxUy='__version__ = "0.0.5"\nimport os\nimport compress_pickle\n\ndef cache_write(object, file_name, verbose=True):\n dn = os.path.dirname(file_name)\n if not os.path.exists(dn):\n os.mkdir(dn)\n if verbose: print("Writing cache...", file_name)\n with open(file_name, \'wb\', ) as f:\n compress_pickle.dump(object, f, compression="lzma")\n if verbose: print("Done!")\n\n\ndef cache_exists(file_name):\n # file_name = cn_(file_name) if cache_prefix else file_name\n return os.path.exists(file_name)\n\n\ndef cache_read(file_name):\n # file_name = cn_(file_name) if cache_prefix else file_name\n if os.path.exists(file_name):\n with open(file_name, \'rb\') as f:\n return compress_pickle.load(f, compression="lzma")\n # return pickle.load(f)\n else:\n return None\n\n\n\n"""\ngit add . && git commit -m "Options" && git push && pip install git+ssh://git@gitlab.compute.dtu.dk/tuhe/unitgrade.git --upgrade\n\n"""\nimport unittest\nimport numpy as np\nimport os\nfrom io import StringIO\nimport sys\nimport collections\nimport inspect\n\nmyround = lambda x: np.round(x) # required.\nmsum = lambda x: sum(x)\nmfloor = lambda x: np.floor(x)\n\ndef setup_dir_by_class(C,base_dir):\n name = C.__class__.__name__\n # base_dir = os.path.join(base_dir, name)\n # if not os.path.isdir(base_dir):\n # os.makedirs(base_dir)\n return base_dir, name\n\nclass Hidden:\n def hide(self):\n return True\n\nimport sys\n\nclass Logger(object):\n def __init__(self, buffer):\n self.terminal = sys.stdout\n self.log = buffer\n\n def write(self, message):\n self.terminal.write(message)\n self.log.write(message)\n\n def flush(self):\n # this flush method is needed for python 3 compatibility.\n pass\n\n# sys.stdout = Logger()\n\nclass Capturing(list):\n def __init__(self, *args, unmute=False, **kwargs):\n self.unmute = unmute\n super().__init__(*args, **kwargs)\n\n def __enter__(self, capture_errors=True): # don\'t put arguments here.\n self._stdout = sys.stdout\n self._stringio = StringIO()\n if self.unmute:\n sys.stdout = Logger(self._stringio)\n else:\n sys.stdout = self._stringio\n\n if capture_errors:\n self._sterr = sys.stderr\n sys.sterr = StringIO() # memory hole it\n self.capture_errors = capture_errors\n return self\n\n def __exit__(self, *args):\n self.extend(self._stringio.getvalue().splitlines())\n del self._stringio # free up some memory\n sys.stdout = self._stdout\n if self.capture_errors:\n sys.sterr = self._sterr\n\n\nclass QItem(unittest.TestCase):\n title = None\n testfun = unittest.TestCase.assertEqual\n tol = 0\n\n def __init__(self, working_directory=None, correct_answer_payload=None, *args, **kwargs):\n self.name = self.__class__.__name__\n self._correct_answer_payload = correct_answer_payload\n super().__init__(*args, **kwargs)\n if self.title is None:\n self.title = self.name\n\n def assertL2(self, computed, expected, tol=None):\n if tol == None:\n tol = self.tol\n diff = np.abs( (np.asarray(computed) - np.asarray(expected)) )\n if np.max(diff) > tol:\n print("Not equal within tolerance {tol}")\n print(f"Element-wise differences {diff.tolist()}")\n self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol}")\n\n def assertL2Relative(self, computed, expected, tol=None):\n if tol == None:\n tol = self.tol\n diff = np.abs( (np.asarray(computed) - np.asarray(expected)) )\n diff = diff / (1e-8 + np.abs( (np.asarray(computed) + np.asarray(expected)) ) )\n if np.sum(diff > tol) > 0:\n print(f"Not equal within tolerance {tol}")\n print(f"Element-wise differences {diff.tolist()}")\n self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol}")\n\n def compute_answer(self, unmute=False):\n raise NotImplementedError("test code here")\n\n def test(self, computed, expected):\n self.testfun(computed, expected)\n\n def get_points(self, verbose=False, show_expected=False, show_computed=False,unmute=False, passall=False, **kwargs):\n possible = 1\n computed = None\n def show_computed_(computed):\n print(">>> Your output:")\n print(computed)\n\n def show_expected_(expected):\n print(">>> Expected output:")\n print(expected)\n\n try:\n if unmute:\n print("\\n")\n computed = self.compute_answer(unmute=unmute)\n except Exception as e:\n if not passall:\n print("\\n=================================================================================")\n print(f"When trying to run test class \'{self.name}\' your code threw an error:")\n print(e)\n print("=================================================================================")\n import traceback\n traceback.print_exc()\n return (0, possible)\n correct = self._correct_answer_payload\n if show_expected or show_computed:\n print("\\n")\n if show_expected:\n show_expected_(correct)\n if show_computed:\n show_computed_(computed)\n try:\n # print(computed, correct)\n # if passall:\n # computed = correct\n if not passall:\n self.test(computed=computed, expected=correct)\n except Exception as e:\n print("\\n=================================================================================")\n print(f"Test output from test class \'{self.name}\' does not match expected result. Test error:")\n print(e)\n show_computed_(computed)\n show_expected_(correct)\n # print("-------------------------Your output:--------------------------------------------")\n # print(computed)\n # print("-------------------------Expected output:----------------------------------------")\n # print(correct)\n # print("=================================================================================")\n return (0, possible)\n return (1, possible)\n\n def score(self):\n try:\n self.test()\n except Exception as e:\n return 0\n return 1\n\nclass QPrintItem(QItem):\n def compute_answer_print(self):\n """\n Generate output which is to be tested. By default, both text written to the terminal using print(...) as well as return values\n are send to process_output (see compute_answer below). In other words, the text generated is:\n\n res = compute_Answer_print()\n txt = (any terminal output generated above)\n numbers = (any numbers found in terminal-output txt)\n\n self.test(process_output(res, txt, numbers), <expected result>)\n\n :return: Optional values for comparison\n """\n raise Exception("Generate output here. The output is passed to self.process_output")\n\n def process_output(self, res, txt, numbers):\n return (res, txt)\n\n def compute_answer(self, unmute=False):\n with Capturing(unmute=unmute) as output:\n res = self.compute_answer_print()\n s = "\\n".join(output)\n numbers = extract_numbers(s)\n return self.process_output(res, s, numbers)\n\nclass OrderedClassMembers(type):\n @classmethod\n def __prepare__(self, name, bases):\n return collections.OrderedDict()\n def __new__(self, name, bases, classdict):\n classdict[\'__ordered__\'] = [key for key in classdict.keys() if key not in (\'__module__\', \'__qualname__\')]\n return type.__new__(self, name, bases, classdict)\n\nclass QuestionGroup(metaclass=OrderedClassMembers):\n title = "Graph search"\n items = None\n partially_scored = False\n\n def __init__(self, *args, **kwargs):\n self.name = self.__class__.__name__\n if self.items is None:\n self.items = []\n members = [gt for gt in [getattr(self, gt) for gt in self.__ordered__] if inspect.isclass(gt) and issubclass(gt, QItem)]\n for gt in members:\n self.items.append( (gt, 1) )\n self.items = [(I(), w) for I, w in self.items]\n\nclass Report():\n title = "report title"\n questions = []\n pack_imports = []\n\n def __init__(self, strict=False,payload=None):\n working_directory = os.path.abspath(os.path.dirname(inspect.getfile(type(self))))\n # working_directory = os.path.join(pathlib.Path(__file__).parent.absolute(), "payloads/")\n self.wdir, self.name = setup_dir_by_class(self, working_directory)\n self.computed_answers_file = os.path.join(self.wdir, self.name + "_resources_do_not_hand_in.dat")\n self.questions = [(Q(working_directory=self.wdir),w) for Q,w in self.questions]\n # self.strict = strict\n if payload is not None:\n self.set_payload(payload, strict=strict)\n else:\n if os.path.isfile(self.computed_answers_file):\n self.set_payload(cache_read(self.computed_answers_file), strict=strict)\n else:\n s = f"> Warning: The pre-computed answer file, {os.path.abspath(self.computed_answers_file)} is missing. The framework will NOT work as intended. Reasons may be a broken local installation."\n if strict:\n raise Exception(s)\n else:\n print(s)\n\n def set_payload(self, payloads, strict=False):\n for q, _ in self.questions:\n for item, _ in q.items:\n if q.name not in payloads or item.name not in payloads[q.name]:\n s = f"> Broken resource dictionary submitted to unitgrade for question {q.name} and subquestion {item.name}. Framework will not work."\n if strict:\n raise Exception(s)\n else:\n print(s)\n item._correct_answer_payload = payloads[q.name][item.name][\'payload\']\n\ndef extract_numbers(txt):\n import re\n numeric_const_pattern = \'[-+]? (?: (?: \\d* \\. \\d+ ) | (?: \\d+ \\.? ) )(?: [Ee] [+-]? \\d+ ) ?\'\n rx = re.compile(numeric_const_pattern, re.VERBOSE)\n all = rx.findall(txt)\n all = [float(a) if \'.\' in a else int(a) for a in all]\n return all\n\n\nimport numpy as np\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport pickle\nimport pyfiglet\n\n# from unitgrade.unitgrade import Hidden\n# import unitgrade as ug\n# import unitgrade.unitgrade as ug\nimport inspect\nimport os\nimport argparse\n\n\nparser = argparse.ArgumentParser(description=\'Evaluate your report.\', epilog="""Example: \nTo run all tests in a report: \n\n> python assignment1_dp.py\n\nTo run only question 2 or question 2.1\n\n> python assignment1_dp.py -q 2\n> python assignment1_dp.py -q 2.1\n\nNote this scripts does not grade your report. To grade your report, use:\n\n> python report1_grade.py\n\nFinally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful.\nAs an old, suppose the report file is Documents/course_package/assignment1_dp.py, and `course_package` is a python package. Change directory to \'Documents/` and execute:\n\n> python -m course_package.report1\n\nsee https://docs.python.org/3.9/using/cmdline.html\n""", formatter_class=argparse.RawTextHelpFormatter)\nparser.add_argument(\'-q\', nargs=\'?\', type=str, default=None, help=\'Only evaluate this question (old: -q 2)\')\nparser.add_argument(\'--showexpected\', action="store_true", help=\'Show the expected/desired result\')\nparser.add_argument(\'--showcomputed\', action="store_true", help=\'Show the answer your code computes\')\nparser.add_argument(\'--unmute\', action="store_true", help=\'Show result of print(...) commands in code\')\nparser.add_argument(\'--passall\', action="store_true", help=\'Automatically pass all tests. Useful when debugging.\')\n\n# parser.add_argument(\'integers\', metavar=\'N\', type=int, nargs=\'+\',\n# help=\'an integer for the accumulator\')\n# parser.add_argument(\'--sum\', dest=\'accumulate\', action=\'store_const\',\n# const=sum, default=max,\n# help=\'sum the integers (default: find the max)\')\n\n\n\ndef evaluate_report_student(report, question=None, qitem=None):\n args = parser.parse_args()\n if question is None and args.q is not None:\n question = args.q\n if "." in question:\n question, qitem = [int(v) for v in question.split(".")]\n else:\n question = int(question)\n\n # print(args)\n results, table_data = evaluate_report(report, question=question, qitem=qitem, verbose=False, passall=args.passall, show_expected=args.showexpected, show_computed=args.showcomputed,unmute=args.unmute)\n\n if question is None:\n print("Provisional evaluation")\n tabulate(table_data)\n table = table_data\n print(tabulate(table))\n print(" ")\n print("Note your results have not yet been registered. \\nTo register your results, please run the file:")\n\n fr = inspect.getouterframes(inspect.currentframe())[1].filename\n print(">>>", os.path.basename(fr)[:-3] + "_grade.py")\n print("In the same manner as you ran this file.")\n return results\n\n\ndef upack(q):\n # h = zip([(i[\'w\'], i[\'possible\'], i[\'obtained\']) for i in q.values()])\n h =[(i[\'w\'], i[\'possible\'], i[\'obtained\']) for i in q.values()]\n h = np.asarray(h)\n return h[:,0], h[:,1], h[:,2],\n #\n # ws, possible, obtained = (np.asarray(x).squeeze() for x in zip([(i[\'w\'], i[\'possible\'], i[\'obtained\']) for i in q.values()]))\n # return ws, possible, obtained\n\ndef evaluate_report(report, question=None, qitem=None, passall=False, verbose=False, show_expected=False, show_computed=False,unmute=False):\n now = datetime.now()\n # show_expected = True\n ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom")\n b = "\\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] )\n print(b + " v" + __version__)\n dt_string = now.strftime("%d/%m/%Y %H:%M:%S")\n print("Started: " + dt_string)\n print("Evaluating " + report.title, "(use --help for options)")\n print(f"Loaded answers from: ", report.computed_answers_file, "\\n")\n table_data = []\n nL = 80\n\n score = {}\n for n, (q, w) in enumerate(report.questions):\n q_hidden = issubclass(q.__class__, Hidden)\n if question is not None and n+1 != question:\n continue\n\n print(f"Question {n+1}: {q.title}")\n print("="*nL)\n q.possible = 0\n q.obtained = 0\n\n q_ = {} # Gather score in this class.\n for j, (item, iw) in enumerate(q.items):\n if qitem is not None and question is not None and item is not None and j+1 != qitem:\n continue\n\n ss = f"*** q{n+1}.{j+1}) {item.title}"\n el = nL-4\n if len(ss) < el:\n ss += \'.\'*(el-len(ss))\n hidden = issubclass(item.__class__, Hidden)\n if not hidden:\n print(ss, end="")\n (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall)\n q_[j] = {\'w\': iw, \'possible\': possible, \'obtained\': current, \'hidden\': hidden}\n # q.possible += possible * iw\n # q.obtained += current * iw\n if not hidden:\n if current == possible:\n print(f"PASS")\n else:\n print(f"*** FAILED")\n\n ws, possible, obtained = upack(q_)\n possible = int(ws @ possible)\n obtained = int(ws @ obtained)\n obtained = int(myround(int((w * obtained) / possible ))) if possible > 0 else 0\n score[n] = {\'w\': w, \'possible\': w, \'obtained\': obtained, \'Ãtems\': q_, \'hidden\': q_hidden}\n\n q.obtained = obtained\n q.possible = possible\n\n s1 = f"*** Question q{n+1}"\n s2 = f" {q.obtained}/{w}"\n print(s1 + ("."* (nL-len(s1)-len(s2) )) + s2 )\n print(" ")\n table_data.append([f"Question q{n+1}", f"{q.obtained}/{w}"])\n\n ws, possible, obtained = upack(score)\n possible = int( msum(possible) )\n obtained = int( msum(obtained) ) # Cast to python int\n report.possible = possible\n report.obtained = obtained\n now = datetime.now()\n dt_string = now.strftime("%H:%M:%S")\n print(f"Completed: "+ dt_string)\n table_data.append(["Total", ""+str(report.obtained)+"/"+str(report.possible) ])\n results = {\'total\': (obtained, possible), \'details\': score}\n return results, table_data\n\n\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport inspect\nimport json\nimport os\nimport bz2\nimport pickle\n\ndef bzwrite(json_str, token): # to get around obfuscation issues\n with getattr(bz2, \'open\')(token, "wt") as f:\n f.write(json_str)\n\ndef gather_upload_to_campusnet(report, output_dir=None):\n n = 80\n results, table_data = evaluate_report(report)\n print(" ")\n print("="*n)\n print("Final evaluation")\n print(tabulate(table_data))\n # also load the source code of missing files...\n results[\'sources\'] = {}\n print("Gathering files...")\n for m in report.pack_imports:\n with open(m.__file__, \'r\') as f:\n results[\'sources\'][m.__name__] = f.read()\n print(f"*** {m.__name__}")\n\n results[\'sources\'] = {}\n json_str = json.dumps(results, indent=4)\n # now = datetime.now()\n # dname = os.path.dirname(inspect.getfile(report.__class__))\n # dname = os.getcwd()\n if output_dir is None:\n output_dir = os.getcwd()\n\n payload_out_base = report.__class__.__name__ + "_handin"\n\n obtain, possible = results[\'total\']\n token = "%s_%i_of_%i.token"%(payload_out_base, obtain, possible) # + str(obtained) +"_" + ".token"\n token = os.path.join(output_dir, token)\n bzwrite(json_str, token)\n\n print(" ")\n print("To get credit for your results, please upload the single file: ")\n print(">", token)\n print("To campusnet without any modifications.")\n\ndef source_instantiate(name, report1_source, payload):\n eval("exec")(report1_source, globals())\n pl = pickle.loads(payload)\n report = eval(name)(payload=pl, strict=True)\n # report.set_payload(pl)\n return report\n\n\nfrom cs101courseware_example import homework1\n\nclass ListReversalQuestion(QuestionGroup):\n title = "Reversal of list"\n\n class ListReversalItem(QPrintItem):\n l = [1, 3, 5, 1, 610]\n def compute_answer_print(self):\n from cs101courseware_example.homework1 import reverse_list\n return reverse_list(self.l)\n\n class ListReversalWordsItem(ListReversalItem):\n l = ["hello", "world", "summer", "dog"]\n\nclass LinearRegressionQuestion(QuestionGroup):\n title = "Linear regression and Boston dataset"\n class CoefficientsItem(QPrintItem):\n testfun = QPrintItem.assertL2\n tol = 0.03\n\n def compute_answer_print(self):\n from cs101courseware_example.homework1 import boston_linear\n boston_linear()\n\n def process_output(self, res, txt, numbers):\n return numbers[:-1]\n\n class RMSEItem(CoefficientsItem):\n def process_output(self, res, txt, numbers):\n return numbers[-1]\n\nclass Report1(Report):\n title = "CS 101 Report 1"\n questions = [(ListReversalQuestion, 5), (LinearRegressionQuestion, 13)]\n pack_imports = [homework1] # Include this file in .token file' -fsQIGnDNmJuSbCgMlxUL=b'\x80\x03}q\x00(X\x14\x00\x00\x00ListReversalQuestionq\x01}q\x02(X\x10\x00\x00\x00ListReversalItemq\x03}q\x04X\x07\x00\x00\x00payloadq\x05]q\x06(Mb\x02K\x01K\x05K\x03K\x01eX\x00\x00\x00\x00q\x07\x86q\x08sX\x15\x00\x00\x00ListReversalWordsItemq\t}q\nh\x05]q\x0b(X\x03\x00\x00\x00dogq\x0cX\x06\x00\x00\x00summerq\rX\x05\x00\x00\x00worldq\x0eX\x05\x00\x00\x00helloq\x0feh\x07\x86q\x10suX\x18\x00\x00\x00LinearRegressionQuestionq\x11}q\x12(X\x10\x00\x00\x00CoefficientsItemq\x13}q\x14h\x05]q\x15(G\xbf\xbb\xa9\xccl\xaa\xa3,G?\xa7\xc2\xcc*\xd6\xe7ZG?\x95\x87{p\x1f\x08\x14G@\x05~\x82\xfb\xf3U\xf9G\xc01\xc9ZT\xe0,%G@\x0e{U\xd8N\xb6\x1cG?Gc\x97&\xd0\xc8mG\xbf\xf7\x99S\xe4\x02\xc9^G?\xd3\x99W\xb5\x1d\xae\x92G\xbf\x89G\x85<\xcf\xc5\xfaG\xbf\xee~\xdc\x03v\x82\x12G?\x83\x11\xdf\xd3\xc9\x16\rG\xbf\xe0\xc9\xe8Q\x0c\xb7\xf4esX\x08\x00\x00\x00RMSEItemq\x16}q\x17h\x05G@\x12\xb72\xbc\x18\x9aqsuu.' -fsQIGnDNmJuSbCgMlxUv="Report1" -fsQIGnDNmJuSbCgMlxUo=fsQIGnDNmJuSbCgMlxcR(fsQIGnDNmJuSbCgMlxUv,fsQIGnDNmJuSbCgMlxUy,fsQIGnDNmJuSbCgMlxUL) -fsQIGnDNmJuSbCgMlxUB=fsQIGnDNmJuSbCgMlxcK.dirname(__file__) -fsQIGnDNmJuSbCgMlxcU(fsQIGnDNmJuSbCgMlxUo,fsQIGnDNmJuSbCgMlxUB) \ No newline at end of file + iFlPxdyCqBrgMNDzVofa,iFlPxdyCqBrgMNDzVofp=iFlPxdyCqBrgMNDzVoEO(iFlPxdyCqBrgMNDzVofu,show_help_flag=iFlPxdyCqBrgMNDzVocY,show_expected=iFlPxdyCqBrgMNDzVocY,show_computed=iFlPxdyCqBrgMNDzVocY,silent=iFlPxdyCqBrgMNDzVocG) + iFlPxdyCqBrgMNDzVocR(" ") + iFlPxdyCqBrgMNDzVocR("="*n) + iFlPxdyCqBrgMNDzVocR("Final evaluation") + iFlPxdyCqBrgMNDzVocR(tabulate(iFlPxdyCqBrgMNDzVofp)) + if iFlPxdyCqBrgMNDzVocO(iFlPxdyCqBrgMNDzVofu.individual_imports)>0: + iFlPxdyCqBrgMNDzVocR("By uploading the .token file, you verify the files:") + for m in iFlPxdyCqBrgMNDzVofu.individual_imports: + iFlPxdyCqBrgMNDzVocR(">",m.__file__) + iFlPxdyCqBrgMNDzVocR("Are created/modified individually by you in agreement with DTUs exam rules") + iFlPxdyCqBrgMNDzVofu.pack_imports+=iFlPxdyCqBrgMNDzVofu.individual_imports + iFlPxdyCqBrgMNDzVoEh={} + if iFlPxdyCqBrgMNDzVocO(iFlPxdyCqBrgMNDzVofu.pack_imports)>0: + iFlPxdyCqBrgMNDzVocR("Including files in upload...") + for m in iFlPxdyCqBrgMNDzVofu.pack_imports: + iFlPxdyCqBrgMNDzVoES =iFlPxdyCqBrgMNDzVoEW(m) + if iFlPxdyCqBrgMNDzVocO([k for k in iFlPxdyCqBrgMNDzVoES if k not in iFlPxdyCqBrgMNDzVoEh])>0: + iFlPxdyCqBrgMNDzVocR(f"*** {m.__name__}") + iFlPxdyCqBrgMNDzVoEh={**iFlPxdyCqBrgMNDzVoEh,**iFlPxdyCqBrgMNDzVoES} + iFlPxdyCqBrgMNDzVofa['sources']=iFlPxdyCqBrgMNDzVoEh + iFlPxdyCqBrgMNDzVoEQ=iFlPxdyCqBrgMNDzVocU(iFlPxdyCqBrgMNDzVofa,indent=4) + if output_dir is iFlPxdyCqBrgMNDzVocQ: + output_dir=iFlPxdyCqBrgMNDzVocn() + iFlPxdyCqBrgMNDzVoET=iFlPxdyCqBrgMNDzVofu.__class__.__name__+"_handin" + iFlPxdyCqBrgMNDzVoEk,iFlPxdyCqBrgMNDzVoEb=iFlPxdyCqBrgMNDzVofa['total'] + iFlPxdyCqBrgMNDzVoEt="_v"+iFlPxdyCqBrgMNDzVofu.version if iFlPxdyCqBrgMNDzVofu.version is not iFlPxdyCqBrgMNDzVocQ else "" + iFlPxdyCqBrgMNDzVoEs="%s_%i_of_%i%s.token"%(iFlPxdyCqBrgMNDzVoET,iFlPxdyCqBrgMNDzVoEk,iFlPxdyCqBrgMNDzVoEb,iFlPxdyCqBrgMNDzVoEt) + iFlPxdyCqBrgMNDzVoEs=iFlPxdyCqBrgMNDzVocw.join(output_dir,iFlPxdyCqBrgMNDzVoEs) + iFlPxdyCqBrgMNDzVoEI(iFlPxdyCqBrgMNDzVoEQ,iFlPxdyCqBrgMNDzVoEs) + iFlPxdyCqBrgMNDzVocR(" ") + iFlPxdyCqBrgMNDzVocR("To get credit for your results, please upload the single file: ") + iFlPxdyCqBrgMNDzVocR(">",iFlPxdyCqBrgMNDzVoEs) + iFlPxdyCqBrgMNDzVocR("To campusnet without any modifications.") +def iFlPxdyCqBrgMNDzVoEH(iFlPxdyCqBrgMNDzVoEu,iFlPxdyCqBrgMNDzVoEL,payload): + iFlPxdyCqBrgMNDzVocW("exec")(iFlPxdyCqBrgMNDzVoEL,iFlPxdyCqBrgMNDzVojf()) + pl=iFlPxdyCqBrgMNDzVoch(iFlPxdyCqBrgMNDzVojE.fromhex(payload)) + iFlPxdyCqBrgMNDzVofu=iFlPxdyCqBrgMNDzVocW(iFlPxdyCqBrgMNDzVoEu)(payload=pl,strict=iFlPxdyCqBrgMNDzVocG) + return iFlPxdyCqBrgMNDzVofu +iFlPxdyCqBrgMNDzVoEL='import os\n\n# DONT\'t import stuff here since install script requires __version__\n\ndef cache_write(object, file_name, verbose=True):\n import compress_pickle\n dn = os.path.dirname(file_name)\n if not os.path.exists(dn):\n os.mkdir(dn)\n if verbose: print("Writing cache...", file_name)\n with open(file_name, \'wb\', ) as f:\n compress_pickle.dump(object, f, compression="lzma")\n if verbose: print("Done!")\n\n\ndef cache_exists(file_name):\n # file_name = cn_(file_name) if cache_prefix else file_name\n return os.path.exists(file_name)\n\n\ndef cache_read(file_name):\n import compress_pickle # Import here because if you import in top the __version__ tag will fail.\n # file_name = cn_(file_name) if cache_prefix else file_name\n if os.path.exists(file_name):\n try:\n with open(file_name, \'rb\') as f:\n return compress_pickle.load(f, compression="lzma")\n except Exception as e:\n print("Tried to load a bad pickle file at", file_name)\n print("If the file appears to be automatically generated, you can try to delete it, otherwise download a new version")\n print(e)\n # return pickle.load(f)\n else:\n return None\n\n\n\n"""\ngit add . && git commit -m "Options" && git push && pip install git+ssh://git@gitlab.compute.dtu.dk/tuhe/unitgrade.git --upgrade\n\n"""\nimport unittest\nimport numpy as np\nimport os\nimport sys\nfrom io import StringIO\nimport collections\nimport inspect\nimport re\nimport threading\nimport tqdm\nimport time\n\nmyround = lambda x: np.round(x) # required.\nmsum = lambda x: sum(x)\nmfloor = lambda x: np.floor(x)\n\ndef setup_dir_by_class(C,base_dir):\n name = C.__class__.__name__\n # base_dir = os.path.join(base_dir, name)\n # if not os.path.isdir(base_dir):\n # os.makedirs(base_dir)\n return base_dir, name\n\nclass Hidden:\n def hide(self):\n return True\n\nclass Logger(object):\n def __init__(self, buffer):\n self.terminal = sys.stdout\n self.log = buffer\n\n def write(self, message):\n self.terminal.write(message)\n self.log.write(message)\n\n def flush(self):\n # this flush method is needed for python 3 compatibility.\n pass\n\nclass Capturing(list):\n def __init__(self, *args, unmute=False, **kwargs):\n self.unmute = unmute\n super().__init__(*args, **kwargs)\n\n def __enter__(self, capture_errors=True): # don\'t put arguments here.\n self._stdout = sys.stdout\n self._stringio = StringIO()\n if self.unmute:\n sys.stdout = Logger(self._stringio)\n else:\n sys.stdout = self._stringio\n\n if capture_errors:\n self._sterr = sys.stderr\n sys.sterr = StringIO() # memory hole it\n self.capture_errors = capture_errors\n return self\n\n def __exit__(self, *args):\n self.extend(self._stringio.getvalue().splitlines())\n del self._stringio # free up some memory\n sys.stdout = self._stdout\n if self.capture_errors:\n sys.sterr = self._sterr\n\n\nclass QItem(unittest.TestCase):\n title = None\n testfun = None\n tol = 0\n estimated_time = 0.42\n _precomputed_payload = None\n _computed_answer = None # Internal helper to later get results.\n # _precomputed_title = None\n\n def __init__(self, working_directory=None, correct_answer_payload=None, question=None, *args, **kwargs):\n if self.tol > 0 and self.testfun is None:\n self.testfun = self.assertL2Relative\n elif self.testfun is None:\n self.testfun = self.assertEqual\n\n self.name = self.__class__.__name__\n self._correct_answer_payload = correct_answer_payload\n self.question = question\n # self.a = "not set"\n\n super().__init__(*args, **kwargs)\n if self.title is None:\n self.title = self.name\n\n def _safe_get_title(self):\n if self._precomputed_title is not None:\n return self._precomputed_title\n return self.title\n\n # def get_title(self):\n # Overwrite this to compute a post-computed title.\n # return None\n\n def assertNorm(self, computed, expected, tol=None):\n if tol == None:\n tol = self.tol\n diff = np.abs( (np.asarray(computed).flat- np.asarray(expected)).flat )\n nrm = np.sqrt(np.sum( diff ** 2))\n\n self.error_computed = nrm\n\n if nrm > tol:\n print(f"Not equal within tolerance {tol}; norm of difference was {nrm}")\n print(f"Element-wise differences {diff.tolist()}")\n self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol}")\n\n def assertL2(self, computed, expected, tol=None):\n if tol == None:\n tol = self.tol\n diff = np.abs( (np.asarray(computed) - np.asarray(expected)) )\n self.error_computed = np.max(diff)\n\n if np.max(diff) > tol:\n print("Not equal within tolerance {tol}")\n print(f"Element-wise differences {diff.tolist()}")\n self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol}")\n\n def assertL2Relative(self, computed, expected, tol=None):\n if tol == None:\n tol = self.tol\n diff = np.abs( (np.asarray(computed) - np.asarray(expected)) )\n diff = diff / (1e-8 + np.abs( (np.asarray(computed) + np.asarray(expected)) ) )\n self.error_computed = np.max(np.abs(diff))\n if np.sum(diff > tol) > 0:\n print(f"Not equal within tolerance {tol}")\n print(f"Element-wise differences {diff.tolist()}")\n self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol}")\n\n def precomputed_payload(self):\n return self._precomputed_payload\n\n def precompute_payload(self):\n # Pre-compute resources to include in tests (useful for getting around rng).\n pass\n\n def compute_answer(self, unmute=False):\n raise NotImplementedError("test code here")\n\n def test(self, computed, expected):\n self.testfun(computed, expected)\n\n def get_points(self, verbose=False, show_expected=False, show_computed=False,unmute=False, passall=False, silent=False, **kwargs):\n possible = 1\n computed = None\n def show_computed_(computed):\n print(">>> Your output:")\n print(computed)\n\n def show_expected_(expected):\n print(">>> Expected output (note: may have been processed; read text script):")\n print(expected)\n\n correct = self._correct_answer_payload\n try:\n if unmute: # Required to not mix together print stuff.\n print("")\n computed = self.compute_answer(unmute=unmute)\n except Exception as e:\n if not passall:\n if not silent:\n print("\\n=================================================================================")\n print(f"When trying to run test class \'{self.name}\' your code threw an error:", e)\n show_expected_(correct)\n import traceback\n print(traceback.format_exc())\n print("=================================================================================")\n return (0, possible)\n\n if self._computed_answer is None:\n self._computed_answer = computed\n\n if show_expected or show_computed:\n print("\\n")\n if show_expected:\n show_expected_(correct)\n if show_computed:\n show_computed_(computed)\n try:\n if not passall:\n self.test(computed=computed, expected=correct)\n except Exception as e:\n if not silent:\n print("\\n=================================================================================")\n print(f"Test output from test class \'{self.name}\' does not match expected result. Test error:")\n print(e)\n show_computed_(computed)\n show_expected_(correct)\n return (0, possible)\n return (1, possible)\n\n def score(self):\n try:\n self.test()\n except Exception as e:\n return 0\n return 1\n\nclass QPrintItem(QItem):\n def compute_answer_print(self):\n """\n Generate output which is to be tested. By default, both text written to the terminal using print(...) as well as return values\n are send to process_output (see compute_answer below). In other words, the text generated is:\n\n res = compute_Answer_print()\n txt = (any terminal output generated above)\n numbers = (any numbers found in terminal-output txt)\n\n self.test(process_output(res, txt, numbers), <expected result>)\n\n :return: Optional values for comparison\n """\n raise Exception("Generate output here. The output is passed to self.process_output")\n\n def process_output(self, res, txt, numbers):\n return res\n\n def compute_answer(self, unmute=False):\n with Capturing(unmute=unmute) as output:\n res = self.compute_answer_print()\n s = "\\n".join(output)\n s = rm_progress_bar(s) # Remove progress bar.\n numbers = extract_numbers(s)\n self._computed_answer = (res, s, numbers)\n return self.process_output(res, s, numbers)\n\nclass OrderedClassMembers(type):\n @classmethod\n def __prepare__(self, name, bases):\n return collections.OrderedDict()\n def __new__(self, name, bases, classdict):\n ks = list(classdict.keys())\n for b in bases:\n ks += b.__ordered__\n classdict[\'__ordered__\'] = [key for key in ks if key not in (\'__module__\', \'__qualname__\')]\n return type.__new__(self, name, bases, classdict)\n\nclass QuestionGroup(metaclass=OrderedClassMembers):\n title = "Graph search"\n items = None\n partially_scored = False\n t_init = 0 # Time spend on initialization (placeholder; set this externally).\n estimated_time = 0.42\n\n def __init__(self, *args, **kwargs):\n\n self.name = self.__class__.__name__\n if self.items is None:\n self.items = []\n members = [gt for gt in [getattr(self, gt) for gt in self.__ordered__ if gt not in ["__classcell__", "__init__"]] if inspect.isclass(gt) and issubclass(gt, QItem)]\n for gt in members:\n self.items.append( (gt, 1) )\n self.items = [(I(question=self), w) for I, w in self.items]\n self.has_called_init_ = False\n\n def init(self):\n # Can be used to set resources relevant for this question instance.\n pass\n\nclass Report():\n title = "report title"\n version = None\n questions = []\n pack_imports = []\n individual_imports = []\n\n def __init__(self, strict=False, payload=None):\n working_directory = os.path.abspath(os.path.dirname(inspect.getfile(type(self))))\n self.wdir, self.name = setup_dir_by_class(self, working_directory)\n self.computed_answers_file = os.path.join(self.wdir, self.name + "_resources_do_not_hand_in.dat")\n import time\n qs = [] # Has to accumulate to new array otherwise the setup/evaluation steps cannot be run in sequence.\n for k, (Q, w) in enumerate(self.questions):\n # print(k, Q)\n start = time.time()\n q = (Q(working_directory=self.wdir), w)\n q[0].t_init = time.time() - start\n # if time.time() -start > 0.2:\n # raise Exception(Q, "Question takes to long to initialize. Use the init() function to set local variables instead")\n # print(time.time()-start)\n qs.append(q)\n self.questions = qs\n # self.questions = [(Q(working_directory=self.wdir),w) for Q,w in self.questions]\n if payload is not None:\n self.set_payload(payload, strict=strict)\n else:\n if os.path.isfile(self.computed_answers_file):\n self.set_payload(cache_read(self.computed_answers_file), strict=strict)\n else:\n s = f"> Warning: The pre-computed answer file, {os.path.abspath(self.computed_answers_file)} is missing. The framework will NOT work as intended. Reasons may be a broken local installation."\n if strict:\n raise Exception(s)\n else:\n print(s)\n\n\n def set_payload(self, payloads, strict=False):\n for q, _ in self.questions:\n for item, _ in q.items:\n if q.name not in payloads or item.name not in payloads[q.name]:\n s = f"> Broken resource dictionary submitted to unitgrade for question {q.name} and subquestion {item.name}. Framework will not work."\n if strict:\n raise Exception(s)\n else:\n print(s)\n else:\n item._correct_answer_payload = payloads[q.name][item.name][\'payload\']\n item.estimated_time = payloads[q.name][item.name].get("time", 1) #"[\'time\']\n q.estimated_time = payloads[q.name].get("time", 1)\n if "precomputed" in payloads[q.name][item.name]: # Consider removing later.\n item._precomputed_payload = payloads[q.name][item.name][\'precomputed\']\n try:\n if "title" in payloads[q.name][item.name]:\n item.title = payloads[q.name][item.name][\'title\']\n except Exception as e:\n pass\n print("bad", e)\n self.payloads = payloads\n\n\ndef rm_progress_bar(txt):\n # More robust version. Apparently length of bar can depend on various factors, so check for order of symbols.\n nlines = []\n for l in txt.splitlines():\n pct = l.find("%")\n ql = False\n if pct > 0:\n i = l.find("|", pct+1)\n if i > 0 and l.find("|", i+1) > 0:\n ql = True\n if not ql:\n nlines.append(l)\n return "\\n".join(nlines)\n\ndef extract_numbers(txt):\n # txt = rm_progress_bar(txt)\n numeric_const_pattern = \'[-+]? (?: (?: \\d* \\. \\d+ ) | (?: \\d+ \\.? ) )(?: [Ee] [+-]? \\d+ ) ?\'\n rx = re.compile(numeric_const_pattern, re.VERBOSE)\n all = rx.findall(txt)\n all = [float(a) if (\'.\' in a or "e" in a) else int(a) for a in all]\n if len(all) > 500:\n print(txt)\n raise Exception("unitgrade.unitgrade.py: Warning, many numbers!", len(all))\n return all\n\n\nclass ActiveProgress():\n def __init__(self, t, start=True, title="my progress bar"):\n self.t = t\n self._running = False\n self.title = title\n self.dt = 0.1\n\n self.n = int(np.round(self.t / self.dt))\n # self.pbar = tqdm.tqdm(total=self.n)\n\n\n if start:\n self.start()\n\n def start(self):\n self._running = True\n self.thread = threading.Thread(target=self.run)\n self.thread.start()\n\n def terminate(self):\n\n\n self._running = False\n self.thread.join()\n if hasattr(self, \'pbar\') and self.pbar is not None:\n self.pbar.update(1)\n self.pbar.close()\n self.pbar=None\n\n sys.stdout.flush()\n\n def run(self):\n self.pbar = tqdm.tqdm(total=self.n, file=sys.stdout, position=0, leave=False, desc=self.title, ncols=100,\n bar_format=\'{l_bar}{bar}| [{elapsed}<{remaining}]\') # , unit_scale=dt, unit=\'seconds\'):\n\n for _ in range(self.n-1): # Don\'t terminate completely; leave bar at 99% done until terminate.\n if not self._running:\n self.pbar.close()\n self.pbar = None\n break\n\n time.sleep(self.dt)\n self.pbar.update(1)\n\n # if self.pbar is not None:\n # self.pbar.close()\n # self.pbar = None\n # for _ in tqdm.tqdm(range(n), file=sys.stdout, position=0, leave=False, desc=self.title, ncols=100, bar_format=\'{l_bar}{bar}| [{elapsed}<{remaining}]\'): #, unit_scale=dt, unit=\'seconds\'):\n\n\nimport numpy as np\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport pyfiglet\n# import unitgrade\n\n# from unitgrade.unitgrade import Hidden\n# import unitgrade as ug\n# import unitgrade.unitgrade as ug\nimport inspect\nimport os\nimport argparse\nimport sys\nimport time\nimport threading # don\'t import Thread bc. of minify issue.\nimport tqdm # don\'t do from tqdm import tqdm because of minify-issue\n#from threading import Thread # This import presents a problem for the minify-code compression tool.\n\nparser = argparse.ArgumentParser(description=\'Evaluate your report.\', epilog="""Example: \nTo run all tests in a report: \n\n> python assignment1_dp.py\n\nTo run only question 2 or question 2.1\n\n> python assignment1_dp.py -q 2\n> python assignment1_dp.py -q 2.1\n\nNote this scripts does not grade your report. To grade your report, use:\n\n> python report1_grade.py\n\nFinally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful.\nFor instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to \'Documents/` and run:\n\n> python -m course_package.report1\n\nsee https://docs.python.org/3.9/using/cmdline.html\n""", formatter_class=argparse.RawTextHelpFormatter)\nparser.add_argument(\'-q\', nargs=\'?\', type=str, default=None, help=\'Only evaluate this question (e.g.: -q 2)\')\nparser.add_argument(\'--showexpected\', action="store_true", help=\'Show the expected/desired result\')\nparser.add_argument(\'--showcomputed\', action="store_true", help=\'Show the answer your code computes\')\nparser.add_argument(\'--unmute\', action="store_true", help=\'Show result of print(...) commands in code\')\nparser.add_argument(\'--passall\', action="store_true", help=\'Automatically pass all tests. Useful when debugging.\')\n\n# parser.add_argument(\'integers\', metavar=\'N\', type=int, nargs=\'+\',\n# help=\'an integer for the accumulator\')\n# parser.add_argument(\'--sum\', dest=\'accumulate\', action=\'store_const\',\n# const=sum, default=max,\n# help=\'sum the integers (default: find the max)\')\n\ndef evaluate_report_student(report, question=None, qitem=None, unmute=None, passall=None, ignore_missing_file=False, show_tol_err=False):\n args = parser.parse_args()\n if question is None and args.q is not None:\n question = args.q\n if "." in question:\n question, qitem = [int(v) for v in question.split(".")]\n else:\n question = int(question)\n\n if not os.path.isfile(report.computed_answers_file) and not ignore_missing_file:\n raise Exception("> Error: The pre-computed answer file", os.path.abspath(report.computed_answers_file), "does not exist. Check your package installation")\n\n if unmute is None:\n unmute = args.unmute\n if passall is None:\n passall = args.passall\n\n results, table_data = evaluate_report(report, question=question, show_progress_bar=not unmute, qitem=qitem, verbose=False, passall=passall, show_expected=args.showexpected, show_computed=args.showcomputed,unmute=unmute,\n show_tol_err=show_tol_err)\n\n try: # For registering stats.\n import irlc.lectures\n import xlwings\n from openpyxl import Workbook\n import pandas as pd\n from collections import defaultdict\n dd = defaultdict(lambda: [])\n error_computed = []\n for k1, (q, _) in enumerate(report.questions):\n for k2, (item, _) in enumerate(q.items):\n dd[\'question_index\'].append(k1)\n dd[\'item_index\'].append(k2)\n dd[\'question\'].append(q.name)\n dd[\'item\'].append(item.name)\n dd[\'tol\'].append(0 if not hasattr(item, \'tol\') else item.tol)\n error_computed.append(0 if not hasattr(item, \'error_computed\') else item.error_computed)\n\n qstats = report.wdir + "/" + report.name + ".xlsx"\n\n if os.path.isfile(qstats):\n d_read = pd.read_excel(qstats).to_dict()\n else:\n d_read = dict()\n\n for k in range(1000):\n key = \'run_\'+str(k)\n if key in d_read:\n dd[key] = list(d_read[\'run_0\'].values())\n else:\n dd[key] = error_computed\n break\n\n workbook = Workbook()\n worksheet = workbook.active\n for col, key in enumerate(dd.keys()):\n worksheet.cell(row=1, column=col+1).value = key\n for row, item in enumerate(dd[key]):\n worksheet.cell(row=row+2, column=col+1).value = item\n\n workbook.save(qstats)\n workbook.close()\n\n except ModuleNotFoundError as e:\n s = 234\n pass\n\n if question is None:\n print("Provisional evaluation")\n tabulate(table_data)\n table = table_data\n print(tabulate(table))\n print(" ")\n\n fr = inspect.getouterframes(inspect.currentframe())[1].filename\n gfile = os.path.basename(fr)[:-3] + "_grade.py"\n if os.path.exists(gfile):\n print("Note your results have not yet been registered. \\nTo register your results, please run the file:")\n print(">>>", gfile)\n print("In the same manner as you ran this file.")\n return results\n\n\ndef upack(q):\n # h = zip([(i[\'w\'], i[\'possible\'], i[\'obtained\']) for i in q.values()])\n h =[(i[\'w\'], i[\'possible\'], i[\'obtained\']) for i in q.values()]\n h = np.asarray(h)\n return h[:,0], h[:,1], h[:,2],\n\n\n\ndef evaluate_report(report, question=None, qitem=None, passall=False, verbose=False, show_expected=False, show_computed=False,unmute=False, show_help_flag=True, silent=False,\n show_progress_bar=True,\n show_tol_err=False):\n now = datetime.now()\n ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom")\n b = "\\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] )\n print(b + " v" + __version__)\n dt_string = now.strftime("%d/%m/%Y %H:%M:%S")\n print("Started: " + dt_string)\n s = report.title\n if report.version is not None:\n s += " version " + report.version\n print("Evaluating " + s, "(use --help for options)" if show_help_flag else "")\n print(f"Loaded answers from: ", report.computed_answers_file, "\\n")\n table_data = []\n nL = 80\n t_start = time.time()\n score = {}\n for n, (q, w) in enumerate(report.questions):\n q_hidden = issubclass(q.__class__, Hidden)\n # report.globals = q.globals\n # q.globals = report.globals\n if question is not None and n+1 != question:\n continue\n\n # Don\'t use f format strings.\n q_title_print = "Question %i: %s"%(n+1, q.title)\n print(q_title_print, end="")\n # sys.stdout.flush()\n q.possible = 0\n q.obtained = 0\n q_ = {} # Gather score in this class.\n # Active progress bar.\n\n for j, (item, iw) in enumerate(q.items):\n if qitem is not None and question is not None and item is not None and j+1 != qitem:\n continue\n if not q.has_called_init_:\n start = time.time()\n\n cc = None\n if show_progress_bar:\n cc = ActiveProgress(t=q.estimated_time, title=q_title_print)\n with eval(\'Capturing\')(unmute=unmute): # Clunky import syntax is required bc. of minify issue.\n try:\n q.init() # Initialize the question. Useful for sharing resources.\n except Exception as e:\n if not passall:\n if not silent:\n print(" ")\n print("="*30)\n print(f"When initializing question {q.title} the initialization code threw an error")\n print(e)\n print("The remaining parts of this question will likely fail.")\n print("="*30)\n\n if show_progress_bar:\n cc.terminate()\n sys.stdout.flush()\n print(q_title_print, end="")\n\n q.has_called_init_ = True\n q_time =np.round( time.time()-start, 2)\n\n print(" "* max(0,nL - len(q_title_print) ) + (" (" + str(q_time) + " seconds)" if q_time >= 0.1 else "") ) # if q.name in report.payloads else "")\n print("=" * nL)\n\n item.question = q # Set the parent question instance for later reference.\n item_title_print = ss = "*** q%i.%i) %s"%(n+1, j+1, item.title)\n\n if show_progress_bar:\n cc = ActiveProgress(t=item.estimated_time, title=item_title_print)\n else:\n print(item_title_print + ( \'.\'*max(0, nL-4-len(ss)) ), end="")\n hidden = issubclass(item.__class__, Hidden)\n # if not hidden:\n # print(ss, end="")\n # sys.stdout.flush()\n start = time.time()\n (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)\n q_[j] = {\'w\': iw, \'possible\': possible, \'obtained\': current, \'hidden\': hidden, \'computed\': str(item._computed_answer), \'title\': item.title}\n tsecs = np.round(time.time()-start, 2)\n if show_progress_bar:\n cc.terminate()\n sys.stdout.flush()\n print(item_title_print + (\'.\' * max(0, nL - 4 - len(ss))), end="")\n\n if not hidden:\n ss = "PASS" if current == possible else "*** FAILED"\n if tsecs >= 0.1:\n ss += " ("+ str(tsecs) + " seconds)"\n print(ss)\n\n ws, possible, obtained = upack(q_)\n possible = int(ws @ possible)\n obtained = int(ws @ obtained)\n obtained = int(myround(int((w * obtained) / possible ))) if possible > 0 else 0\n score[n] = {\'w\': w, \'possible\': w, \'obtained\': obtained, \'items\': q_, \'hidden\': q_hidden, \'title\': q.title}\n\n q.obtained = obtained\n q.possible = possible\n\n s1 = f"*** Question q{n+1}"\n s2 = f" {q.obtained}/{w}"\n print(s1 + ("."* (nL-len(s1)-len(s2) )) + s2 )\n print(" ")\n table_data.append([f"Question q{n+1}", f"{q.obtained}/{w}"])\n\n ws, possible, obtained = upack(score)\n possible = int( msum(possible) )\n obtained = int( msum(obtained) ) # Cast to python int\n report.possible = possible\n report.obtained = obtained\n now = datetime.now()\n dt_string = now.strftime("%H:%M:%S")\n\n dt = int(time.time()-t_start)\n minutes = dt//60\n seconds = dt - minutes*60\n plrl = lambda i, s: str(i) + " " + s + ("s" if i != 1 else "")\n\n print(f"Completed: "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +")")\n\n table_data.append(["Total", ""+str(report.obtained)+"/"+str(report.possible) ])\n results = {\'total\': (obtained, possible), \'details\': score}\n return results, table_data\n\n\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport inspect\nimport json\nimport os\nimport bz2\nimport pickle\nimport os\n\n\ndef bzwrite(json_str, token): # to get around obfuscation issues\n with getattr(bz2, \'open\')(token, "wt") as f:\n f.write(json_str)\n\ndef gather_imports(imp):\n resources = {}\n m = imp\n # for m in pack_imports:\n # print(f"*** {m.__name__}")\n f = m.__file__\n # dn = os.path.dirname(f)\n top_package = os.path.dirname(__import__(m.__name__.split(\'.\')[0]).__file__)\n # top_package = os.path.dirname(__import__(m.__name__.split(\'.\')[0]).__file__)\n top_package = os.path.dirname(top_package)\n\n if f.endswith("__init__.py"):\n for root, dirs, files in os.walk(os.path.dirname(f)):\n for file in files:\n if file.endswith(".py"):\n # print(file)\n # print()\n v = os.path.relpath(os.path.join(root, file), top_package)\n with open(os.path.join(root, file), \'r\') as ff:\n resources[v] = ff.read()\n else:\n v = os.path.relpath(f, top_package)\n with open(f, \'r\') as ff:\n resources[v] = ff.read()\n return resources\n\n\ndef gather_upload_to_campusnet(report, output_dir=None):\n n = 80\n results, table_data = evaluate_report(report, show_help_flag=False, show_expected=False, show_computed=False, silent=True)\n print(" ")\n print("="*n)\n print("Final evaluation")\n print(tabulate(table_data))\n # also load the source code of missing files...\n\n if len(report.individual_imports) > 0:\n print("By uploading the .token file, you verify the files:")\n for m in report.individual_imports:\n print(">", m.__file__)\n print("Are created/modified individually by you in agreement with DTUs exam rules")\n report.pack_imports += report.individual_imports\n\n sources = {}\n if len(report.pack_imports) > 0:\n print("Including files in upload...")\n for m in report.pack_imports:\n nimp = gather_imports(m)\n if len([k for k in nimp if k not in sources]) > 0:\n print(f"*** {m.__name__}")\n sources = {**sources, **nimp}\n results[\'sources\'] = sources\n\n json_str = json.dumps(results, indent=4)\n\n if output_dir is None:\n output_dir = os.getcwd()\n\n payload_out_base = report.__class__.__name__ + "_handin"\n\n obtain, possible = results[\'total\']\n vstring = "_v"+report.version if report.version is not None else ""\n\n token = "%s_%i_of_%i%s.token"%(payload_out_base, obtain, possible,vstring)\n token = os.path.join(output_dir, token)\n bzwrite(json_str, token)\n\n print(" ")\n print("To get credit for your results, please upload the single file: ")\n print(">", token)\n print("To campusnet without any modifications.")\n\ndef source_instantiate(name, report1_source, payload):\n eval("exec")(report1_source, globals())\n pl = pickle.loads(bytes.fromhex(payload))\n report = eval(name)(payload=pl, strict=True)\n # report.set_payload(pl)\n return report\n\n\n__version__ = "0.1.8"\n\nfrom cs101courseware_example import homework1\n\nclass ListReversalQuestion(QuestionGroup):\n title = "Reversal of list"\n\n class ListReversalItem(QPrintItem):\n l = [1, 3, 5, 1, 610]\n def compute_answer_print(self):\n from cs101courseware_example.homework1 import reverse_list\n return reverse_list(self.l)\n\n class ListReversalWordsItem(ListReversalItem):\n l = ["hello", "world", "summer", "dog"]\n\nclass LinearRegressionQuestion(QuestionGroup):\n title = "Linear regression and Boston dataset"\n class CoefficientsItem(QPrintItem):\n testfun = QPrintItem.assertL2\n tol = 0.03\n\n def compute_answer_print(self):\n from cs101courseware_example.homework1 import boston_linear\n boston_linear()\n\n def process_output(self, res, txt, numbers):\n return numbers[:-1]\n\n class RMSEItem(CoefficientsItem):\n def process_output(self, res, txt, numbers):\n return numbers[-1]\n\nclass Report1(Report):\n title = "CS 101 Report 1"\n questions = [(ListReversalQuestion, 5), (LinearRegressionQuestion, 13)]\n pack_imports = [homework1] # Include this file in .token file' +iFlPxdyCqBrgMNDzVoEA='80049512020000000000007d94288c144c697374526576657273616c5175657374696f6e947d94288c0474696d65944700000000000000008c104c697374526576657273616c4974656d947d94288c077061796c6f6164945d94284d62024b014b054b034b01658c0b707265636f6d7075746564944e68034700000000000000008c057469746c65948c104c697374526576657273616c4974656d94758c154c697374526576657273616c576f7264734974656d947d942868065d94288c03646f67948c0673756d6d6572948c05776f726c64948c0568656c6c6f946568084e680347000000000000000068098c154c697374526576657273616c576f7264734974656d9475758c184c696e65617252656772657373696f6e5175657374696f6e947d942868034700000000000000008c10436f656666696369656e74734974656d947d942868065d942847bfbbad207494a76c473fa7c5437cbda6fb473f951ff08b42e9b547400582027fe20d7c47c031c19fcb0c026d47400e7c9dd6eb08cd473f47049a406460ce47bff79a05a5307b9e473fd396c01163bbcd47bf8944115fa064ea47bfee7c69c063070b473f8318255bc9455447bfe0ca713e6cde616568084e6803473f8f2b200000000068098c10436f656666696369656e74734974656d94758c08524d53454974656d947d94286806474012b794bed3e1f168084e6803473f9ec3e00000000068098c08524d53454974656d947575752e' +iFlPxdyCqBrgMNDzVoEu="Report1" +iFlPxdyCqBrgMNDzVofu=iFlPxdyCqBrgMNDzVoEH(iFlPxdyCqBrgMNDzVoEu,iFlPxdyCqBrgMNDzVoEL,iFlPxdyCqBrgMNDzVoEA) +iFlPxdyCqBrgMNDzVoEY=iFlPxdyCqBrgMNDzVocw.dirname(__file__) +iFlPxdyCqBrgMNDzVoEe(iFlPxdyCqBrgMNDzVofu,iFlPxdyCqBrgMNDzVoEY) \ No newline at end of file diff --git a/cs101courseware_example/cs101report2_grade.py b/cs101courseware_example/cs101report2_grade.py index 540fff65035e19d7fcd30a119d3cbbc1a354bd88..68392fed6ebaa911078b5488f8f939611b24d662 100644 --- a/cs101courseware_example/cs101report2_grade.py +++ b/cs101courseware_example/cs101report2_grade.py @@ -1,3 +1,3 @@ '''WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt.''' import bz2, base64 -exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/cs101courseware_example/instructions.py b/cs101courseware_example/instructions.py index 68f05cca062614b0bfe2cb2ef3c4dc44f9771a2b..b6e149be16a4ead2a4efbc9bf1f431851c31c74b 100644 --- a/cs101courseware_example/instructions.py +++ b/cs101courseware_example/instructions.py @@ -20,7 +20,7 @@ for d in os.listdir(wdir): 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 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 cs101 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}"') print("Then run the script as usual") tprint(f'python cs101report1.py') diff --git a/unitgrade/Report_resources_do_not_hand_in.dat b/unitgrade/Report_resources_do_not_hand_in.dat new file mode 100644 index 0000000000000000000000000000000000000000..9c15fa98decdca1da9d38c1989086f8c4c1ce960 Binary files /dev/null and b/unitgrade/Report_resources_do_not_hand_in.dat differ diff --git a/unitgrade/__init__.py b/unitgrade/__init__.py index f59054178dcb91369f13288d1571a4f2ab10a9ea..dacfb2d7865656a202efd9b2f0965d4482172bcb 100644 --- a/unitgrade/__init__.py +++ b/unitgrade/__init__.py @@ -1,4 +1,3 @@ -from unitgrade.version import __version__ import os # DONT't import stuff here since install script requires __version__ diff --git a/unitgrade/__pycache__/unitgrade.cpython-38.pyc b/unitgrade/__pycache__/unitgrade.cpython-38.pyc index 49ec789f81c61461975b3223eaa6b5b60596fd78..75fca3f30359a786a8edcc4725b1b6e9cff4b706 100644 Binary files a/unitgrade/__pycache__/unitgrade.cpython-38.pyc and b/unitgrade/__pycache__/unitgrade.cpython-38.pyc differ diff --git a/unitgrade/__pycache__/unitgrade_helpers.cpython-38.pyc b/unitgrade/__pycache__/unitgrade_helpers.cpython-38.pyc index 712bb3bb40300a849453141c82e5f9afc3bac086..f096dbdbc9ef5f84f60d6d6f8229efe6b68c7e04 100644 Binary files a/unitgrade/__pycache__/unitgrade_helpers.cpython-38.pyc and b/unitgrade/__pycache__/unitgrade_helpers.cpython-38.pyc differ diff --git a/unitgrade/__pycache__/version.cpython-38.pyc b/unitgrade/__pycache__/version.cpython-38.pyc index 74567f3b5e3d3f07730bb308aaec25f3d90a3ad9..60c21c4fd046cdd33eb3c601e3df33ed76e6e12f 100644 Binary files a/unitgrade/__pycache__/version.cpython-38.pyc and b/unitgrade/__pycache__/version.cpython-38.pyc differ diff --git a/unitgrade/unitgrade.py b/unitgrade/unitgrade.py index f829f0a809d8dc743bfdf1ed848b8512b2ad4f28..a56b4a7601d2d4333ef29e37b0f985fffe902809 100644 --- a/unitgrade/unitgrade.py +++ b/unitgrade/unitgrade.py @@ -77,23 +77,27 @@ class QItem(unittest.TestCase): estimated_time = 0.42 _precomputed_payload = None _computed_answer = None # Internal helper to later get results. - # _precomputed_payload = None + weight = 1 # the weight of the question. - def __init__(self, working_directory=None, correct_answer_payload=None, question=None, *args, **kwargs): + def __init__(self, question=None, *args, **kwargs): if self.tol > 0 and self.testfun is None: self.testfun = self.assertL2Relative elif self.testfun is None: self.testfun = self.assertEqual self.name = self.__class__.__name__ - self._correct_answer_payload = correct_answer_payload + # self._correct_answer_payload = correct_answer_payload self.question = question - # self.a = "not set" super().__init__(*args, **kwargs) if self.title is None: self.title = self.name + def _safe_get_title(self): + if self._precomputed_title is not None: + return self._precomputed_title + return self.title + def assertNorm(self, computed, expected, tol=None): if tol == None: tol = self.tol @@ -114,9 +118,9 @@ class QItem(unittest.TestCase): self.error_computed = np.max(diff) if np.max(diff) > tol: - print("Not equal within tolerance {tol}") + print(f"Not equal within tolerance {tol=}; deviation was {np.max(diff)=}") print(f"Element-wise differences {diff.tolist()}") - self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol}") + self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol=}, {np.max(diff)=}") def assertL2Relative(self, computed, expected, tol=None): if tol == None: @@ -215,10 +219,7 @@ class QPrintItem(QItem): raise Exception("Generate output here. The output is passed to self.process_output") def process_output(self, res, txt, numbers): - return (res, txt) - - # def compute_local(self): # Dunno - # pass + return res def compute_answer(self, unmute=False): with Capturing(unmute=unmute) as output: @@ -234,30 +235,55 @@ class OrderedClassMembers(type): def __prepare__(self, name, bases): return collections.OrderedDict() def __new__(self, name, bases, classdict): - classdict['__ordered__'] = [key for key in classdict.keys() if key not in ('__module__', '__qualname__')] + ks = list(classdict.keys()) + for b in bases: + ks += b.__ordered__ + classdict['__ordered__'] = [key for key in ks if key not in ('__module__', '__qualname__')] return type.__new__(self, name, bases, classdict) class QuestionGroup(metaclass=OrderedClassMembers): - title = "Graph search" - items = None + title = "Untitled question" partially_scored = False t_init = 0 # Time spend on initialization (placeholder; set this externally). estimated_time = 0.42 - - def __init__(self, *args, **kwargs): - self.name = self.__class__.__name__ - if self.items is None: - self.items = [] + has_called_init_ = False + _name = None + _items = None + + @property + def items(self): + if self._items == None: + self._items = [] members = [gt for gt in [getattr(self, gt) for gt in self.__ordered__ if gt not in ["__classcell__", "__init__"]] if inspect.isclass(gt) and issubclass(gt, QItem)] - for gt in members: - self.items.append( (gt, 1) ) - self.items = [(I(question=self), w) for I, w in self.items] - self.has_called_init_ = False + for I in members: + self._items.append( I(question=self)) + return self._items + + @items.setter + def items(self, value): + self._items = value + + @property + def name(self): + if self._name == None: + self._name = self.__class__.__name__ + return self._name # + + @name.setter + def name(self, val): + self._name = val def init(self): # Can be used to set resources relevant for this question instance. pass + def init_all_item_questions(self): + for item in self.items: + if not item.question.has_called_init_: + item.question.init() + item.question.has_called_init_ = True + + class Report(): title = "report title" version = None @@ -272,16 +298,14 @@ class Report(): import time qs = [] # Has to accumulate to new array otherwise the setup/evaluation steps cannot be run in sequence. for k, (Q, w) in enumerate(self.questions): - # print(k, Q) start = time.time() - q = (Q(working_directory=self.wdir), w) - q[0].t_init = time.time() - start - # if time.time() -start > 0.2: - # raise Exception(Q, "Question takes to long to initialize. Use the init() function to set local variables instead") - # print(time.time()-start) - qs.append(q) + q = Q() + q.t_init = time.time() - start + for k, i in enumerate(q.items): + i.name = i.name + "_" + str(k) + qs.append((q, w)) + self.questions = qs - # self.questions = [(Q(working_directory=self.wdir),w) for Q,w in self.questions] if payload is not None: self.set_payload(payload, strict=strict) else: @@ -297,7 +321,7 @@ class Report(): def set_payload(self, payloads, strict=False): for q, _ in self.questions: - for item, _ in q.items: + for item in q.items: if q.name not in payloads or item.name not in payloads[q.name]: s = f"> Broken resource dictionary submitted to unitgrade for question {q.name} and subquestion {item.name}. Framework will not work." if strict: @@ -306,10 +330,16 @@ class Report(): print(s) else: item._correct_answer_payload = payloads[q.name][item.name]['payload'] - item.estimated_time = payloads[q.name][item.name]['time'] - q.estimated_time = payloads[q.name]['time'] + item.estimated_time = payloads[q.name][item.name].get("time", 1) + q.estimated_time = payloads[q.name].get("time", 1) if "precomputed" in payloads[q.name][item.name]: # Consider removing later. item._precomputed_payload = payloads[q.name][item.name]['precomputed'] + try: + if "title" in payloads[q.name][item.name]: # can perhaps be removed later. + item.title = payloads[q.name][item.name]['title'] + except Exception as e: # Cannot set attribute error. The title is a function (and probably should not be). + pass + # print("bad", e) self.payloads = payloads @@ -382,8 +412,3 @@ class ActiveProgress(): time.sleep(self.dt) self.pbar.update(1) - - # if self.pbar is not None: - # self.pbar.close() - # self.pbar = None - # for _ in tqdm.tqdm(range(n), file=sys.stdout, position=0, leave=False, desc=self.title, ncols=100, bar_format='{l_bar}{bar}| [{elapsed}<{remaining}]'): #, unit_scale=dt, unit='seconds'): diff --git a/unitgrade/unitgrade_grade.py b/unitgrade/unitgrade_grade.py new file mode 100644 index 0000000000000000000000000000000000000000..a9c09c14fcbd3872350e34b3d8c9a24bc4278ce1 --- /dev/null +++ b/unitgrade/unitgrade_grade.py @@ -0,0 +1,3 @@ +'''WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt.''' +import bz2, base64 +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/unitgrade/unitgrade_helpers.py b/unitgrade/unitgrade_helpers.py index f14c2b7332a44bd49881991c97b10b88bea71792..d5b1c536ec033f773397c8bab57ec0f423d27da3 100644 --- a/unitgrade/unitgrade_helpers.py +++ b/unitgrade/unitgrade_helpers.py @@ -2,9 +2,8 @@ import numpy as np from tabulate import tabulate from datetime import datetime import pyfiglet -from unitgrade import Hidden, myround, msum, mfloor, ActiveProgress +from unitgrade import Hidden, myround, msum, ActiveProgress # import unitgrade -from unitgrade import __version__ # from unitgrade.unitgrade import Hidden # import unitgrade as ug @@ -14,8 +13,7 @@ import os import argparse import sys 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 + #from threading import Thread # This import presents a problem for the minify-code compression tool. parser = argparse.ArgumentParser(description='Evaluate your report.', epilog="""Example: @@ -33,7 +31,7 @@ Note this scripts does not grade your report. To grade your report, use: > python report1_grade.py Finally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful. -For instance, if the report file is in Documents/course_package/report1.py, and `course_package` is a python package, then change directory to 'Documents/` and run: +For instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to 'Documents/` and run: > python -m course_package.report1 @@ -81,7 +79,7 @@ def evaluate_report_student(report, question=None, qitem=None, unmute=None, pass dd = defaultdict(lambda: []) error_computed = [] for k1, (q, _) in enumerate(report.questions): - for k2, (item, _) in enumerate(q.items): + for k2, item in enumerate(q.items): dd['question_index'].append(k1) dd['item_index'].append(k2) dd['question'].append(q.name) @@ -145,7 +143,7 @@ def upack(q): def evaluate_report(report, question=None, qitem=None, passall=False, verbose=False, show_expected=False, show_computed=False,unmute=False, show_help_flag=True, silent=False, show_progress_bar=True, show_tol_err=False): - from unitgrade.version import __version__ + from src.snipper.version import __version__ now = datetime.now() ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom") b = "\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] ) @@ -163,33 +161,35 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa score = {} for n, (q, w) in enumerate(report.questions): q_hidden = issubclass(q.__class__, Hidden) - # report.globals = q.globals - # q.globals = report.globals if question is not None and n+1 != question: continue - - # Don't use f format strings. q_title_print = "Question %i: %s"%(n+1, q.title) print(q_title_print, end="") - # sys.stdout.flush() q.possible = 0 q.obtained = 0 q_ = {} # Gather score in this class. - # Active progress bar. - for j, (item, iw) in enumerate(q.items): - if qitem is not None and question is not None and item is not None and j+1 != qitem: + q_with_outstanding_init = [item.question for item in q.items if not item.question.has_called_init_] + + for j, item in enumerate(q.items): + if qitem is not None and question is not None and j+1 != qitem: continue - if not q.has_called_init_: + + if q_with_outstanding_init is not None: # check for None bc. this must be called to set titles. + # if not item.question.has_called_init_: start = time.time() cc = None if show_progress_bar: - cc = ActiveProgress(t=q.estimated_time, title=q_title_print) - from unitgrade import Capturing + total_estimated_time = q.estimated_time # Use this. The time is estimated for the q itself. # sum( [q2.estimated_time for q2 in q_with_outstanding_init] ) + cc = ActiveProgress(t=total_estimated_time, title=q_title_print) with eval('Capturing')(unmute=unmute): # Clunky import syntax is required bc. of minify issue. try: - q.init() # Initialize the question. Useful for sharing resources. + for q2 in q_with_outstanding_init: + q2.init() + q2.has_called_init_ = True + + # item.question.init() # Initialize the question. Useful for sharing resources. except Exception as e: if not passall: if not silent: @@ -205,13 +205,14 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa sys.stdout.flush() print(q_title_print, end="") - q.has_called_init_ = True + # item.question.has_called_init_ = True q_time =np.round( time.time()-start, 2) print(" "* max(0,nL - len(q_title_print) ) + (" (" + str(q_time) + " seconds)" if q_time >= 0.1 else "") ) # if q.name in report.payloads else "") print("=" * nL) + q_with_outstanding_init = None - item.question = q # Set the parent question instance for later reference. + # item.question = q # Set the parent question instance for later reference. item_title_print = ss = "*** q%i.%i) %s"%(n+1, j+1, item.title) if show_progress_bar: @@ -223,8 +224,9 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa # print(ss, end="") # sys.stdout.flush() start = time.time() + (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent) - q_[j] = {'w': iw, 'possible': possible, 'obtained': current, 'hidden': hidden, 'computed': str(item._computed_answer), 'title': item.title} + q_[j] = {'w': item.weight, 'possible': possible, 'obtained': current, 'hidden': hidden, 'computed': str(item._computed_answer), 'title': item.title} tsecs = np.round(time.time()-start, 2) if show_progress_bar: cc.terminate() diff --git a/unitgrade/version.py b/unitgrade/version.py index 283b03a075bdc493860ebb799f4b2f3ee2a72d87..a23ef3f48b85172c0ab83ad6a8b4cea4fd584c04 100644 --- a/unitgrade/version.py +++ b/unitgrade/version.py @@ -1 +1 @@ -__version__ = "0.1.7" \ No newline at end of file +__version__ = "0.1.8" \ No newline at end of file diff --git a/unitgrade2/__init__.py b/unitgrade2/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..23b1c9d945462ead9ebec34c1c4da50e8906cf24 --- /dev/null +++ b/unitgrade2/__init__.py @@ -0,0 +1,37 @@ +from unitgrade2.version import __version__ +import os + +# DONT't import stuff here since install script requires __version__ + +def cache_write(object, file_name, verbose=True): + import compress_pickle + dn = os.path.dirname(file_name) + if not os.path.exists(dn): + os.mkdir(dn) + if verbose: print("Writing cache...", file_name) + with open(file_name, 'wb', ) as f: + compress_pickle.dump(object, f, compression="lzma") + if verbose: print("Done!") + + +def cache_exists(file_name): + # file_name = cn_(file_name) if cache_prefix else file_name + return os.path.exists(file_name) + + +def cache_read(file_name): + import compress_pickle # Import here because if you import in top the __version__ tag will fail. + # file_name = cn_(file_name) if cache_prefix else file_name + if os.path.exists(file_name): + try: + with open(file_name, 'rb') as f: + return compress_pickle.load(f, compression="lzma") + except Exception as e: + print("Tried to load a bad pickle file at", file_name) + print("If the file appears to be automatically generated, you can try to delete it, otherwise download a new version") + print(e) + # return pickle.load(f) + else: + return None + +from unitgrade2.unitgrade2 import Hidden, myround, mfloor, msum, Capturing, ActiveProgress diff --git a/unitgrade2/__pycache__/__init__.cpython-38.pyc b/unitgrade2/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac7b672193da207e651f61b4bee3e1791e27a656 Binary files /dev/null and b/unitgrade2/__pycache__/__init__.cpython-38.pyc differ diff --git a/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc b/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa46009fef3a824583373629a6d857240757d712 Binary files /dev/null and b/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc differ diff --git a/unitgrade2/__pycache__/unitgrade_helpers2.cpython-38.pyc b/unitgrade2/__pycache__/unitgrade_helpers2.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85a4965da5d65114c8709bc88a2d0be4810d2f9e Binary files /dev/null and b/unitgrade2/__pycache__/unitgrade_helpers2.cpython-38.pyc differ diff --git a/unitgrade2/__pycache__/version.cpython-38.pyc b/unitgrade2/__pycache__/version.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e804a3c2f2261406205db1c73302a4acb70b698 Binary files /dev/null and b/unitgrade2/__pycache__/version.cpython-38.pyc differ diff --git a/unitgrade2/unitgrade/ListQuestion.pkl b/unitgrade2/unitgrade/ListQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..b201d4b453d94b66ed44ac821d72534a90b09811 Binary files /dev/null and b/unitgrade2/unitgrade/ListQuestion.pkl differ diff --git a/unitgrade2/unitgrade2.py b/unitgrade2/unitgrade2.py new file mode 100644 index 0000000000000000000000000000000000000000..95b071c1146b8c9a20558db65599ec262c09363c --- /dev/null +++ b/unitgrade2/unitgrade2.py @@ -0,0 +1,811 @@ +""" +git add . && git commit -m "Options" && git push && pip install git+ssh://git@gitlab.compute.dtu.dk/tuhe/unitgrade.git --upgrade + +""" +from . import cache_read +import unittest +import numpy as np +import os +import sys +from io import StringIO +import collections +import inspect +import re +import threading +import tqdm +import time +import pickle +import itertools + +myround = lambda x: np.round(x) # required. +msum = lambda x: sum(x) +mfloor = lambda x: np.floor(x) + +def setup_dir_by_class(C,base_dir): + name = C.__class__.__name__ + # base_dir = os.path.join(base_dir, name) + # if not os.path.isdir(base_dir): + # os.makedirs(base_dir) + return base_dir, name + +class Hidden: + def hide(self): + return True + +class Logger(object): + def __init__(self, buffer): + self.terminal = sys.stdout + self.log = buffer + + def write(self, message): + self.terminal.write(message) + self.log.write(message) + + def flush(self): + # this flush method is needed for python 3 compatibility. + pass + +class Capturing(list): + def __init__(self, *args, unmute=False, **kwargs): + self.unmute = unmute + super().__init__(*args, **kwargs) + + def __enter__(self, capture_errors=True): # don't put arguments here. + self._stdout = sys.stdout + self._stringio = StringIO() + if self.unmute: + sys.stdout = Logger(self._stringio) + else: + sys.stdout = self._stringio + + if capture_errors: + self._sterr = sys.stderr + sys.sterr = StringIO() # memory hole it + self.capture_errors = capture_errors + return self + + def __exit__(self, *args): + self.extend(self._stringio.getvalue().splitlines()) + del self._stringio # free up some memory + sys.stdout = self._stdout + if self.capture_errors: + sys.sterr = self._sterr + + +class QItem(unittest.TestCase): + title = None + testfun = None + tol = 0 + estimated_time = 0.42 + _precomputed_payload = None + _computed_answer = None # Internal helper to later get results. + weight = 1 # the weight of the question. + + def __init__(self, question=None, *args, **kwargs): + if self.tol > 0 and self.testfun is None: + self.testfun = self.assertL2Relative + elif self.testfun is None: + self.testfun = self.assertEqual + + self.name = self.__class__.__name__ + # self._correct_answer_payload = correct_answer_payload + self.question = question + + super().__init__(*args, **kwargs) + if self.title is None: + self.title = self.name + + def _safe_get_title(self): + if self._precomputed_title is not None: + return self._precomputed_title + return self.title + + def assertNorm(self, computed, expected, tol=None): + if tol == None: + tol = self.tol + diff = np.abs( (np.asarray(computed).flat- np.asarray(expected)).flat ) + nrm = np.sqrt(np.sum( diff ** 2)) + + self.error_computed = nrm + + if nrm > tol: + print(f"Not equal within tolerance {tol}; norm of difference was {nrm}") + print(f"Element-wise differences {diff.tolist()}") + self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol}") + + def assertL2(self, computed, expected, tol=None): + if tol == None: + tol = self.tol + diff = np.abs( (np.asarray(computed) - np.asarray(expected)) ) + self.error_computed = np.max(diff) + + if np.max(diff) > tol: + print(f"Not equal within tolerance {tol=}; deviation was {np.max(diff)=}") + print(f"Element-wise differences {diff.tolist()}") + self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol=}, {np.max(diff)=}") + + def assertL2Relative(self, computed, expected, tol=None): + if tol == None: + tol = self.tol + diff = np.abs( (np.asarray(computed) - np.asarray(expected)) ) + diff = diff / (1e-8 + np.abs( (np.asarray(computed) + np.asarray(expected)) ) ) + self.error_computed = np.max(np.abs(diff)) + if np.sum(diff > tol) > 0: + print(f"Not equal within tolerance {tol}") + print(f"Element-wise differences {diff.tolist()}") + self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol}") + + def precomputed_payload(self): + return self._precomputed_payload + + def precompute_payload(self): + # Pre-compute resources to include in tests (useful for getting around rng). + pass + + def compute_answer(self, unmute=False): + raise NotImplementedError("test code here") + + def test(self, computed, expected): + self.testfun(computed, expected) + + def get_points(self, verbose=False, show_expected=False, show_computed=False,unmute=False, passall=False, silent=False, **kwargs): + possible = 1 + computed = None + def show_computed_(computed): + print(">>> Your output:") + print(computed) + + def show_expected_(expected): + print(">>> Expected output (note: may have been processed; read text script):") + print(expected) + + correct = self._correct_answer_payload + try: + if unmute: # Required to not mix together print stuff. + print("") + computed = self.compute_answer(unmute=unmute) + except Exception as e: + if not passall: + if not silent: + print("\n=================================================================================") + print(f"When trying to run test class '{self.name}' your code threw an error:", e) + show_expected_(correct) + import traceback + print(traceback.format_exc()) + print("=================================================================================") + return (0, possible) + + if self._computed_answer is None: + self._computed_answer = computed + + if show_expected or show_computed: + print("\n") + if show_expected: + show_expected_(correct) + if show_computed: + show_computed_(computed) + try: + if not passall: + self.test(computed=computed, expected=correct) + except Exception as e: + if not silent: + print("\n=================================================================================") + print(f"Test output from test class '{self.name}' does not match expected result. Test error:") + print(e) + show_computed_(computed) + show_expected_(correct) + return (0, possible) + return (1, possible) + + def score(self): + try: + self.test() + except Exception as e: + return 0 + return 1 + +class QPrintItem(QItem): + def compute_answer_print(self): + """ + Generate output which is to be tested. By default, both text written to the terminal using print(...) as well as return values + are send to process_output (see compute_answer below). In other words, the text generated is: + + res = compute_Answer_print() + txt = (any terminal output generated above) + numbers = (any numbers found in terminal-output txt) + + self.test(process_output(res, txt, numbers), <expected result>) + + :return: Optional values for comparison + """ + raise Exception("Generate output here. The output is passed to self.process_output") + + def process_output(self, res, txt, numbers): + return res + + def compute_answer(self, unmute=False): + with Capturing(unmute=unmute) as output: + res = self.compute_answer_print() + s = "\n".join(output) + s = rm_progress_bar(s) # Remove progress bar. + numbers = extract_numbers(s) + self._computed_answer = (res, s, numbers) + return self.process_output(res, s, numbers) + +class OrderedClassMembers(type): + @classmethod + def __prepare__(self, name, bases): + return collections.OrderedDict() + def __new__(self, name, bases, classdict): + ks = list(classdict.keys()) + for b in bases: + ks += b.__ordered__ + classdict['__ordered__'] = [key for key in ks if key not in ('__module__', '__qualname__')] + return type.__new__(self, name, bases, classdict) + +class QuestionGroup(metaclass=OrderedClassMembers): + title = "Untitled question" + partially_scored = False + t_init = 0 # Time spend on initialization (placeholder; set this externally). + estimated_time = 0.42 + has_called_init_ = False + _name = None + _items = None + + @property + def items(self): + if self._items == None: + self._items = [] + members = [gt for gt in [getattr(self, gt) for gt in self.__ordered__ if gt not in ["__classcell__", "__init__"]] if inspect.isclass(gt) and issubclass(gt, QItem)] + for I in members: + self._items.append( I(question=self)) + return self._items + + @items.setter + def items(self, value): + self._items = value + + @property + def name(self): + if self._name == None: + self._name = self.__class__.__name__ + return self._name # + + @name.setter + def name(self, val): + self._name = val + + def init(self): + # Can be used to set resources relevant for this question instance. + pass + + def init_all_item_questions(self): + for item in self.items: + if not item.question.has_called_init_: + item.question.init() + item.question.has_called_init_ = True + + +class Report(): + title = "report title" + version = None + questions = [] + pack_imports = [] + individual_imports = [] + + @classmethod + def reset(cls): + for (q,_) in cls.questions: + if hasattr(q, 'reset'): + q.reset() + + def _file(self): + return inspect.getfile(type(self)) + + def __init__(self, strict=False, payload=None): + working_directory = os.path.abspath(os.path.dirname(self._file())) + + self.wdir, self.name = setup_dir_by_class(self, working_directory) + # self.computed_answers_file = os.path.join(self.wdir, self.name + "_resources_do_not_hand_in.dat") + + if payload is not None: + self.set_payload(payload, strict=strict) + # else: + # if os.path.isfile(self.computed_answers_file): + # self.set_payload(cache_read(self.computed_answers_file), strict=strict) + # else: + # s = f"> Warning: The pre-computed answer file, {os.path.abspath(self.computed_answers_file)} is missing. The framework will NOT work as intended. Reasons may be a broken local installation." + # if strict: + # raise Exception(s) + # else: + # print(s) + + def main(self, verbosity=1): + # Run all tests using standard unittest (nothing fancy). + import unittest + loader = unittest.TestLoader() + for q,_ in self.questions: + import time + start = time.time() # A good proxy for setup time is to + suite = loader.loadTestsFromTestCase(q) + unittest.TextTestRunner(verbosity=verbosity).run(suite) + total = time.time() - start + q.time = total + + def _setup_answers(self): + self.main() # Run all tests in class just to get that out of the way... + report_cache = {} + for q, _ in self.questions: + if hasattr(q, '_save_cache'): + q()._save_cache() + q._cache['time'] = q.time + report_cache[q.__qualname__] = q._cache + else: + report_cache[q.__qualname__] = {'no cache see _setup_answers in unitgrade2.py':True} + return report_cache + + def set_payload(self, payloads, strict=False): + for q, _ in self.questions: + q._cache = payloads[q.__qualname__] + + # for item in q.items: + # if q.name not in payloads or item.name not in payloads[q.name]: + # s = f"> Broken resource dictionary submitted to unitgrade for question {q.name} and subquestion {item.name}. Framework will not work." + # if strict: + # raise Exception(s) + # else: + # print(s) + # else: + # item._correct_answer_payload = payloads[q.name][item.name]['payload'] + # item.estimated_time = payloads[q.name][item.name].get("time", 1) + # q.estimated_time = payloads[q.name].get("time", 1) + # if "precomputed" in payloads[q.name][item.name]: # Consider removing later. + # item._precomputed_payload = payloads[q.name][item.name]['precomputed'] + # try: + # if "title" in payloads[q.name][item.name]: # can perhaps be removed later. + # item.title = payloads[q.name][item.name]['title'] + # except Exception as e: # Cannot set attribute error. The title is a function (and probably should not be). + # pass + # # print("bad", e) + # self.payloads = payloads + + +def rm_progress_bar(txt): + # More robust version. Apparently length of bar can depend on various factors, so check for order of symbols. + nlines = [] + for l in txt.splitlines(): + pct = l.find("%") + ql = False + if pct > 0: + i = l.find("|", pct+1) + if i > 0 and l.find("|", i+1) > 0: + ql = True + if not ql: + nlines.append(l) + return "\n".join(nlines) + +def extract_numbers(txt): + # txt = rm_progress_bar(txt) + numeric_const_pattern = '[-+]? (?: (?: \d* \. \d+ ) | (?: \d+ \.? ) )(?: [Ee] [+-]? \d+ ) ?' + rx = re.compile(numeric_const_pattern, re.VERBOSE) + all = rx.findall(txt) + all = [float(a) if ('.' in a or "e" in a) else int(a) for a in all] + if len(all) > 500: + print(txt) + raise Exception("unitgrade.unitgrade.py: Warning, many numbers!", len(all)) + return all + + +class ActiveProgress(): + def __init__(self, t, start=True, title="my progress bar"): + self.t = t + self._running = False + self.title = title + self.dt = 0.1 + self.n = int(np.round(self.t / self.dt)) + # self.pbar = tqdm.tqdm(total=self.n) + if start: + self.start() + + def start(self): + self._running = True + self.thread = threading.Thread(target=self.run) + self.thread.start() + self.time_started = time.time() + + def terminate(self): + self._running = False + self.thread.join() + if hasattr(self, 'pbar') and self.pbar is not None: + self.pbar.update(1) + self.pbar.close() + self.pbar=None + + sys.stdout.flush() + return time.time() - self.time_started + + def run(self): + self.pbar = tqdm.tqdm(total=self.n, file=sys.stdout, position=0, leave=False, desc=self.title, ncols=100, + bar_format='{l_bar}{bar}| [{elapsed}<{remaining}]') # , unit_scale=dt, unit='seconds'): + + for _ in range(self.n-1): # Don't terminate completely; leave bar at 99% done until terminate. + if not self._running: + self.pbar.close() + self.pbar = None + break + + time.sleep(self.dt) + self.pbar.update(1) + + + +from unittest.suite import _isnotsuite + +class MySuite(unittest.suite.TestSuite): # Not sure we need this one anymore. + pass + +def instance_call_stack(instance): + s = "-".join(map(lambda x: x.__name__, instance.__class__.mro())) + return s + +def get_class_that_defined_method(meth): + for cls in inspect.getmro(meth.im_class): + if meth.__name__ in cls.__dict__: + return cls + return None + +def caller_name(skip=2): + """Get a name of a caller in the format module.class.method + + `skip` specifies how many levels of stack to skip while getting caller + name. skip=1 means "who calls me", skip=2 "who calls my caller" etc. + + An empty string is returned if skipped levels exceed stack height + """ + stack = inspect.stack() + start = 0 + skip + if len(stack) < start + 1: + return '' + parentframe = stack[start][0] + + name = [] + module = inspect.getmodule(parentframe) + # `modname` can be None when frame is executed directly in console + # TODO(techtonik): consider using __main__ + if module: + name.append(module.__name__) + # detect classname + if 'self' in parentframe.f_locals: + # I don't know any way to detect call from the object method + # XXX: there seems to be no way to detect static method call - it will + # be just a function call + name.append(parentframe.f_locals['self'].__class__.__name__) + codename = parentframe.f_code.co_name + if codename != '<module>': # top level usually + name.append( codename ) # function or a method + + ## Avoid circular refs and frame leaks + # https://docs.python.org/2.7/library/inspect.html#the-interpreter-stack + del parentframe, stack + + return ".".join(name) + +def get_class_from_frame(fr): + import inspect + args, _, _, value_dict = inspect.getargvalues(fr) + # we check the first parameter for the frame function is + # named 'self' + if len(args) and args[0] == 'self': + # in that case, 'self' will be referenced in value_dict + instance = value_dict.get('self', None) + if instance: + # return its class + # isinstance(instance, Testing) # is the actual class instance. + + return getattr(instance, '__class__', None) + # return None otherwise + return None + +from typing import Any +import inspect, gc + +def giveupthefunc(): + frame = inspect.currentframe() + code = frame.f_code + globs = frame.f_globals + functype = type(lambda: 0) + funcs = [] + for func in gc.get_referrers(code): + if type(func) is functype: + if getattr(func, "__code__", None) is code: + if getattr(func, "__globals__", None) is globs: + funcs.append(func) + if len(funcs) > 1: + return None + return funcs[0] if funcs else None + + +from collections import defaultdict + +class UTextResult(unittest.TextTestResult): + def __init__(self, stream, descriptions, verbosity): + super().__init__(stream, descriptions, verbosity) + self.successes = [] + + def printErrors(self) -> None: + # if self.dots or self.showAll: + # self.stream.writeln() + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + + def addSuccess(self, test: unittest.case.TestCase) -> None: + # super().addSuccess(test) + self.successes.append(test) + # super().addSuccess(test) + # hidden = issubclass(item.__class__, Hidden) + # # if not hidden: + # # print(ss, end="") + # # sys.stdout.flush() + # start = time.time() + # + # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent) + # q_[j] = {'w': item.weight, 'possible': possible, 'obtained': current, 'hidden': hidden, 'computed': str(item._computed_answer), 'title': item.title} + # tsecs = np.round(time.time()-start, 2) + show_progress_bar = True + nL = 80 + if show_progress_bar: + tsecs = np.round( self.cc.terminate(), 2) + sys.stdout.flush() + ss = self.item_title_print + print(self.item_title_print + ('.' * max(0, nL - 4 - len(ss))), end="") + # + # if not hidden: + current = 1 + possible = 1 + # tsecs = 2 + ss = "PASS" if current == possible else "*** FAILED" + if tsecs >= 0.1: + ss += " ("+ str(tsecs) + " seconds)" + print(ss) + + + def startTest(self, test): + # super().startTest(test) + self.testsRun += 1 + # print("Starting the test...") + show_progress_bar = True + n = 1 + j = 1 + item_title = self.getDescription(test) + item_title = item_title.split("\n")[0] + self.item_title_print = "*** q%i.%i) %s" % (n + 1, j + 1, item_title) + estimated_time = 10 + nL = 80 + # + if show_progress_bar: + self.cc = ActiveProgress(t=estimated_time, title=self.item_title_print) + else: + print(self.item_title_print + ('.' * max(0, nL - 4 - len(self.item_title_print))), end="") + + self._test = test + + def _setupStdout(self): + if self._previousTestClass == None: + total_estimated_time = 2 + if hasattr(self.__class__, 'q_title_print'): + q_title_print = self.__class__.q_title_print + else: + q_title_print = "<unnamed test. See unitgrade.py>" + + # q_title_print = "some printed title..." + cc = ActiveProgress(t=total_estimated_time, title=q_title_print) + self.cc = cc + + def _restoreStdout(self): # Used when setting up the test. + if self._previousTestClass == None: + q_time = self.cc.terminate() + q_time = np.round(q_time, 2) + sys.stdout.flush() + print(self.cc.title, end="") + # start = 10 + # q_time = np.round(time.time() - start, 2) + nL = 80 + print(" " * max(0, nL - len(self.cc.title)) + ( + " (" + str(q_time) + " seconds)" if q_time >= 0.1 else "")) # if q.name in report.payloads else "") + print("=" * nL) + +from unittest.runner import _WritelnDecorator +from io import StringIO + +class UTextTestRunner(unittest.TextTestRunner): + def __init__(self, *args, **kwargs): + from io import StringIO + stream = StringIO() + super().__init__(*args, stream=stream, **kwargs) + + def _makeResult(self): + stream = self.stream # not you! + stream = sys.stdout + stream = _WritelnDecorator(stream) + return self.resultclass(stream, self.descriptions, self.verbosity) + +def wrapper(foo): + def magic(self): + s = "-".join(map(lambda x: x.__name__, self.__class__.mro())) + # print(s) + foo(self) + magic.__doc__ = foo.__doc__ + return magic + +from functools import update_wrapper, _make_key, RLock +from collections import namedtuple +_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) + +def cache(foo, typed=False): + """ Magic cache wrapper + https://github.com/python/cpython/blob/main/Lib/functools.py + """ + maxsize = None + def wrapper(self, *args, **kwargs): + key = self.cache_id() + ("cache", _make_key(args, kwargs, typed)) + if not self._cache_contains(key): + value = foo(self, *args, **kwargs) + self._cache_put(key, value) + else: + value = self._cache_get(key) + return value + return wrapper + + +class UTestCase(unittest.TestCase): + _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache. + _cache = None # Read-only cache. + _cache2 = None # User-written cache + + @classmethod + def reset(cls): + cls._outcome = None + cls._cache = None + cls._cache2 = None + + def _get_outcome(self): + if not (self.__class__, '_outcome') or self.__class__._outcome == None: + self.__class__._outcome = {} + return self.__class__._outcome + + def _callTestMethod(self, testMethod): + t = time.time() + res = testMethod() + elapsed = time.time() - t + # if res == None: + # res = {} + # res['time'] = elapsed + sd = self.shortDescription() + self._cache_put( (self.cache_id(), 'title'), self._testMethodName if sd == None else sd) + # self._test_fun_output = res + self._get_outcome()[self.cache_id()] = res + self._cache_put( (self.cache_id(), "time"), elapsed) + + + # This is my base test class. So what is new about it? + def cache_id(self): + c = self.__class__.__qualname__ + m = self._testMethodName + return (c,m) + + def unique_cache_id(self): + k0 = self.cache_id() + key = () + for i in itertools.count(): + key = k0 + (i,) + if not self._cache2_contains(key): + break + return key + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._load_cache() + self.cache_indexes = defaultdict(lambda: 0) + + def _ensure_cache_exists(self): + if not hasattr(self.__class__, '_cache') or self.__class__._cache == None: + self.__class__._cache = dict() + if not hasattr(self.__class__, '_cache2') or self.__class__._cache2 == None: + self.__class__._cache2 = dict() + + def _cache_get(self, key, default=None): + self._ensure_cache_exists() + return self.__class__._cache.get(key, default) + + def _cache_put(self, key, value): + self._ensure_cache_exists() + self.__class__._cache2[key] = value + + def _cache_contains(self, key): + self._ensure_cache_exists() + return key in self.__class__._cache + + def _cache2_contains(self, key): + self._ensure_cache_exists() + return key in self.__class__._cache2 + + def assertEqualC(self, first: Any, msg: Any = ...) -> None: + id = self.unique_cache_id() + if not self._cache_contains(id): + print("Warning, framework missing key", id) + + self.assertEqual(first, self._cache_get(id, first), msg) + self._cache_put(id, first) + + def _cache_file(self): + return os.path.dirname(inspect.getfile(self.__class__) ) + "/unitgrade/" + self.__class__.__name__ + ".pkl" + + def _save_cache(self): + # get the class name (i.e. what to save to). + cfile = self._cache_file() + if not os.path.isdir(os.path.dirname(cfile)): + os.makedirs(os.path.dirname(cfile)) + + if hasattr(self.__class__, '_cache2'): + with open(cfile, 'wb') as f: + pickle.dump(self.__class__._cache2, f) + + # But you can also set cache explicitly. + def _load_cache(self): + if self._cache != None: # Cache already loaded. We will not load it twice. + return + # raise Exception("Loaded cache which was already set. What is going on?!") + cfile = self._cache_file() + print("Loading cache from", cfile) + if os.path.exists(cfile): + with open(cfile, 'rb') as f: + data = pickle.load(f) + self.__class__._cache = data + else: + print("Warning! data file not found", cfile) + +def hide(func): + return func + +def makeRegisteringDecorator(foreignDecorator): + """ + Returns a copy of foreignDecorator, which is identical in every + way(*), except also appends a .decorator property to the callable it + spits out. + """ + def newDecorator(func): + # Call to newDecorator(method) + # Exactly like old decorator, but output keeps track of what decorated it + R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done + R.decorator = newDecorator # keep track of decorator + # R.original = func # might as well keep track of everything! + return R + + newDecorator.__name__ = foreignDecorator.__name__ + newDecorator.__doc__ = foreignDecorator.__doc__ + # (*)We can be somewhat "hygienic", but newDecorator still isn't signature-preserving, i.e. you will not be able to get a runtime list of parameters. For that, you need hackish libraries...but in this case, the only argument is func, so it's not a big issue + return newDecorator + +hide = makeRegisteringDecorator(hide) + +def methodsWithDecorator(cls, decorator): + """ + Returns all methods in CLS with DECORATOR as the + outermost decorator. + + DECORATOR must be a "registering decorator"; one + can make any decorator "registering" via the + makeRegisteringDecorator function. + + import inspect + ls = list(methodsWithDecorator(GeneratorQuestion, deco)) + for f in ls: + print(inspect.getsourcelines(f) ) # How to get all hidden questions. + """ + for maybeDecorated in cls.__dict__.values(): + if hasattr(maybeDecorated, 'decorator'): + if maybeDecorated.decorator == decorator: + print(maybeDecorated) + yield maybeDecorated + diff --git a/unitgrade2/unitgrade_helpers2.py b/unitgrade2/unitgrade_helpers2.py new file mode 100644 index 0000000000000000000000000000000000000000..17004c300d5d43337f2829c343521eca34ffd8fb --- /dev/null +++ b/unitgrade2/unitgrade_helpers2.py @@ -0,0 +1,307 @@ +import numpy as np +from tabulate import tabulate +from datetime import datetime +import pyfiglet +from unitgrade2 import Hidden, myround, msum, mfloor, ActiveProgress +from unitgrade2 import __version__ +import unittest +from unitgrade2.unitgrade2 import MySuite +from unitgrade2.unitgrade2 import UTextResult + +import inspect +import os +import argparse +import sys +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: +To run all tests in a report: + +> python assignment1_dp.py + +To run only question 2 or question 2.1 + +> python assignment1_dp.py -q 2 +> python assignment1_dp.py -q 2.1 + +Note this scripts does not grade your report. To grade your report, use: + +> python report1_grade.py + +Finally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful. +For instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to 'Documents/` and run: + +> python -m course_package.report1 + +see https://docs.python.org/3.9/using/cmdline.html +""", formatter_class=argparse.RawTextHelpFormatter) +parser.add_argument('-q', nargs='?', type=str, default=None, help='Only evaluate this question (e.g.: -q 2)') +parser.add_argument('--showexpected', action="store_true", help='Show the expected/desired result') +parser.add_argument('--showcomputed', action="store_true", help='Show the answer your code computes') +parser.add_argument('--unmute', action="store_true", help='Show result of print(...) commands in code') +parser.add_argument('--passall', action="store_true", help='Automatically pass all tests. Useful when debugging.') + + + +def evaluate_report_student(report, question=None, qitem=None, unmute=None, passall=None, ignore_missing_file=False, show_tol_err=False): + args = parser.parse_args() + if question is None and args.q is not None: + question = args.q + if "." in question: + question, qitem = [int(v) for v in question.split(".")] + else: + question = int(question) + + if hasattr(report, "computed_answer_file") and not os.path.isfile(report.computed_answers_file) and not ignore_missing_file: + raise Exception("> Error: The pre-computed answer file", os.path.abspath(report.computed_answers_file), "does not exist. Check your package installation") + + if unmute is None: + unmute = args.unmute + if passall is None: + passall = args.passall + + results, table_data = evaluate_report(report, question=question, show_progress_bar=not unmute, qitem=qitem, verbose=False, passall=passall, show_expected=args.showexpected, show_computed=args.showcomputed,unmute=unmute, + show_tol_err=show_tol_err) + + + # try: # For registering stats. + # import unitgrade_private + # import irlc.lectures + # import xlwings + # from openpyxl import Workbook + # import pandas as pd + # from collections import defaultdict + # dd = defaultdict(lambda: []) + # error_computed = [] + # for k1, (q, _) in enumerate(report.questions): + # for k2, item in enumerate(q.items): + # dd['question_index'].append(k1) + # dd['item_index'].append(k2) + # dd['question'].append(q.name) + # dd['item'].append(item.name) + # dd['tol'].append(0 if not hasattr(item, 'tol') else item.tol) + # error_computed.append(0 if not hasattr(item, 'error_computed') else item.error_computed) + # + # qstats = report.wdir + "/" + report.name + ".xlsx" + # + # if os.path.isfile(qstats): + # d_read = pd.read_excel(qstats).to_dict() + # else: + # d_read = dict() + # + # for k in range(1000): + # key = 'run_'+str(k) + # if key in d_read: + # dd[key] = list(d_read['run_0'].values()) + # else: + # dd[key] = error_computed + # break + # + # workbook = Workbook() + # worksheet = workbook.active + # for col, key in enumerate(dd.keys()): + # worksheet.cell(row=1, column=col+1).value = key + # for row, item in enumerate(dd[key]): + # worksheet.cell(row=row+2, column=col+1).value = item + # + # workbook.save(qstats) + # workbook.close() + # + # except ModuleNotFoundError as e: + # s = 234 + # pass + + if question is None: + print("Provisional evaluation") + tabulate(table_data) + table = table_data + print(tabulate(table)) + print(" ") + + fr = inspect.getouterframes(inspect.currentframe())[1].filename + gfile = os.path.basename(fr)[:-3] + "_grade.py" + if os.path.exists(gfile): + print("Note your results have not yet been registered. \nTo register your results, please run the file:") + print(">>>", gfile) + print("In the same manner as you ran this file.") + + + return results + + +def upack(q): + # h = zip([(i['w'], i['possible'], i['obtained']) for i in q.values()]) + h =[(i['w'], i['possible'], i['obtained']) for i in q.values()] + h = np.asarray(h) + return h[:,0], h[:,1], h[:,2], + +class UnitgradeTextRunner(unittest.TextTestRunner): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +def evaluate_report(report, question=None, qitem=None, passall=False, verbose=False, show_expected=False, show_computed=False,unmute=False, show_help_flag=True, silent=False, + show_progress_bar=True, + show_tol_err=False): + from unitgrade2.version import __version__ + now = datetime.now() + ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom") + b = "\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] ) + print(b + " v" + __version__) + dt_string = now.strftime("%d/%m/%Y %H:%M:%S") + print("Started: " + dt_string) + s = report.title + if hasattr(report, "version") and report.version is not None: + s += " version " + report.version + print("Evaluating " + s, "(use --help for options)" if show_help_flag else "") + # print(f"Loaded answers from: ", report.computed_answers_file, "\n") + table_data = [] + nL = 80 + t_start = time.time() + score = {} + + # Use the sequential test loader instead. See here: + class SequentialTestLoader(unittest.TestLoader): + def getTestCaseNames(self, testCaseClass): + test_names = super().getTestCaseNames(testCaseClass) + testcase_methods = list(testCaseClass.__dict__.keys()) + test_names.sort(key=testcase_methods.index) + return test_names + loader = SequentialTestLoader() + # loader = unittest.TestLoader() + # loader.suiteClass = MySuite + + 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: + continue + suite = loader.loadTestsFromTestCase(q) + # print(suite) + qtitle = q.__name__ + # qtitle = q.title if hasattr(q, "title") else q.id() + # q.title = qtitle + q_title_print = "Question %i: %s"%(n+1, qtitle) + print(q_title_print, end="") + q.possible = 0 + q.obtained = 0 + q_ = {} # Gather score in this class. + from 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 + res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite) + # res = UTextTestRunner(verbosity=2, resultclass=unittest.TextTestResult).run(suite) + z = 234 + # for j, item in enumerate(q.items): + # if qitem is not None and question is not None and j+1 != qitem: + # continue + # + # if q_with_outstanding_init is not None: # check for None bc. this must be called to set titles. + # # if not item.question.has_called_init_: + # start = time.time() + # + # cc = None + # if show_progress_bar: + # total_estimated_time = q.estimated_time # Use this. The time is estimated for the q itself. # sum( [q2.estimated_time for q2 in q_with_outstanding_init] ) + # cc = ActiveProgress(t=total_estimated_time, title=q_title_print) + # from unitgrade import Capturing # DON'T REMOVE THIS LINE + # with eval('Capturing')(unmute=unmute): # Clunky import syntax is required bc. of minify issue. + # try: + # for q2 in q_with_outstanding_init: + # q2.init() + # q2.has_called_init_ = True + # + # # item.question.init() # Initialize the question. Useful for sharing resources. + # except Exception as e: + # if not passall: + # if not silent: + # print(" ") + # print("="*30) + # print(f"When initializing question {q.title} the initialization code threw an error") + # print(e) + # print("The remaining parts of this question will likely fail.") + # print("="*30) + # + # if show_progress_bar: + # cc.terminate() + # sys.stdout.flush() + # print(q_title_print, end="") + # + # q_time =np.round( time.time()-start, 2) + # + # print(" "* max(0,nL - len(q_title_print) ) + (" (" + str(q_time) + " seconds)" if q_time >= 0.1 else "") ) # if q.name in report.payloads else "") + # print("=" * nL) + # q_with_outstanding_init = None + # + # # item.question = q # Set the parent question instance for later reference. + # item_title_print = ss = "*** q%i.%i) %s"%(n+1, j+1, item.title) + # + # if show_progress_bar: + # cc = ActiveProgress(t=item.estimated_time, title=item_title_print) + # else: + # print(item_title_print + ( '.'*max(0, nL-4-len(ss)) ), end="") + # hidden = issubclass(item.__class__, Hidden) + # # if not hidden: + # # print(ss, end="") + # # sys.stdout.flush() + # start = time.time() + # + # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent) + # q_[j] = {'w': item.weight, 'possible': possible, 'obtained': current, 'hidden': hidden, 'computed': str(item._computed_answer), 'title': item.title} + # tsecs = np.round(time.time()-start, 2) + # if show_progress_bar: + # cc.terminate() + # sys.stdout.flush() + # print(item_title_print + ('.' * max(0, nL - 4 - len(ss))), end="") + # + # if not hidden: + # ss = "PASS" if current == possible else "*** FAILED" + # if tsecs >= 0.1: + # ss += " ("+ str(tsecs) + " seconds)" + # print(ss) + + # ws, possible, obtained = upack(q_) + + possible = res.testsRun + obtained = possible - len(res.errors) + + + # possible = int(ws @ possible) + # obtained = int(ws @ obtained) + # obtained = int(myround(int((w * obtained) / possible ))) if possible > 0 else 0 + + obtained = w * int(obtained * 1.0 / possible ) + score[n] = {'w': w, 'possible': w, 'obtained': obtained, 'items': q_, 'title': qtitle} + q.obtained = obtained + q.possible = possible + + s1 = f"*** Question q{n+1}" + s2 = f" {q.obtained}/{w}" + print(s1 + ("."* (nL-len(s1)-len(s2) )) + s2 ) + print(" ") + table_data.append([f"Question q{n+1}", f"{q.obtained}/{w}"]) + + ws, possible, obtained = upack(score) + possible = int( msum(possible) ) + obtained = int( msum(obtained) ) # Cast to python int + report.possible = possible + report.obtained = obtained + now = datetime.now() + dt_string = now.strftime("%H:%M:%S") + + dt = int(time.time()-t_start) + minutes = dt//60 + seconds = dt - minutes*60 + plrl = lambda i, s: str(i) + " " + s + ("s" if i != 1 else "") + + print(f"Completed: "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +")") + + table_data.append(["Total", ""+str(report.obtained)+"/"+str(report.possible) ]) + results = {'total': (obtained, possible), 'details': score} + return results, table_data + + diff --git a/unitgrade2/version.py b/unitgrade2/version.py new file mode 100644 index 0000000000000000000000000000000000000000..acb984f2d0682c8a7cfc890ce91bbbc229d98abe --- /dev/null +++ b/unitgrade2/version.py @@ -0,0 +1 @@ +__version__ = "0.9.0" \ No newline at end of file