From 2b248fe64c3ca1961d8eca2e1b32ca421b22c75b Mon Sep 17 00:00:00 2001 From: Tue Herlau <tuhe@dtu.dk> Date: Thu, 26 Aug 2021 11:48:41 +0200 Subject: [PATCH] Reorg for new version --- README.md | 84 ++++++++++++++++-- .../cs103/__pycache__/report3.cpython-38.pyc | Bin 1056 -> 1056 bytes .../example_docker/instructor/cs103/deploy.py | 19 ++-- .../cs103/report3_complete_grade.py | 2 +- .../instructor/cs103/report3_grade.py | 2 +- .../tmp/cs103/Report3_handin_0_of_20.token | Bin 138232 -> 138232 bytes .../tmp/cs103/Report3_handin_20_of_20.token | Bin 138309 -> 0 bytes .../__pycache__/homework1.cpython-38.pyc | Bin 922 -> 922 bytes .../report3_complete_grade.cpython-38.pyc | Bin 57919 -> 57919 bytes .../__pycache__/report3_grade.cpython-38.pyc | Bin 57802 -> 0 bytes .../tmp/cs103/report3_complete_grade.py | 2 +- .../tmp/cs103/report3_grade.py | 2 +- .../cs103/Report3_handin_0_of_20.token | Bin 70152 -> 70152 bytes .../__pycache__/homework1.cpython-38.pyc | Bin 992 -> 992 bytes .../cs103/__pycache__/report3.cpython-38.pyc | Bin 1056 -> 1056 bytes .../__pycache__/report3_grade.cpython-38.pyc | Bin 57934 -> 57934 bytes .../students/cs103/report3_grade.py | 2 +- .../instructor/cs102/deploy.py | 2 +- .../instructor/cs101/deploy.py | 5 +- .../__pycache__/docker_helpers.cpython-38.pyc | Bin 1953 -> 1987 bytes unitgrade_private2/docker_helpers.py | 5 +- 21 files changed, 94 insertions(+), 31 deletions(-) delete mode 100644 examples/example_docker/instructor/unitgrade-docker/tmp/cs103/Report3_handin_20_of_20.token delete mode 100644 examples/example_docker/instructor/unitgrade-docker/tmp/cs103/__pycache__/report3_grade.cpython-38.pyc diff --git a/README.md b/README.md index 732ee86..3f88deb 100644 --- a/README.md +++ b/README.md @@ -31,23 +31,89 @@ Let's look at an example. You need three files ``` instructor/cs101/homework.py # This contains the students homework instructor/cs101/report1.py # This contains the tests -instructor/cs101/deploy.py # This deploys the tests +instructor/cs101/deploy.py # A private file to deploy the tests ``` ### The homework -The homework is just any old python code. +The homework is just any old python code you would give to the students. For instance: ```python -def add(a,b): - # Write a function which add two numbers! - return a+b # naturally, this part would NOT be destributed to students +def reverse_list(mylist): #!f + """ + Given a list 'mylist' returns a list consisting of the same elements in reverse order. E.g. + reverse_list([1,2,3]) should return [3,2,1] (as a list). + """ + return list(reversed(mylist)) + +def add(a,b): #!f + """ Given two numbers `a` and `b` this function should simply return their sum: + > add(a,b) = a+b """ + return a+b + +if __name__ == "__main__": + # Problem 1: Write a function which add two numbers + print(f"Your result of 2 + 2 = {add(2,2)}") + print(f"Reversing a small list", reverse_list([2,3,5,7])) ``` ### The test: -The test consists of individual problems and a report-class. The tests themselves are just regular Unittest. For instance: +The test consists of individual problems and a report-class. The tests themselves are just regular Unittest (we will see a slightly smarter idea in a moment). For instance: +```python +from homework1 import reverse_list, add +import unittest + +class Week1(unittest.TestCase): + def test_add(self): + self.assertEqual(add(2,2), 4) + self.assertEqual(add(-100, 5), -95) + + def test_reverse(self): + self.assertEqual(reverse_list([1,2,3]), [3,2,1]) + +``` +A number of tests can be collected into a `Report`, which will allow us to assign points to the tests and use the more advanced features of the framework later. A complete, minimal example: + ```python +from unitgrade2.unitgrade2 import Report +from unitgrade2.unitgrade_helpers2 import evaluate_report_student +from homework1 import reverse_list, add import unittest -class MyTest(unittest.TestCase): - # Write a function which add two numbers! - return a+b # naturally, this part would NOT be destributed to students + +class Week1(unittest.TestCase): + def test_add(self): + self.assertEqual(add(2,2), 4) + self.assertEqual(add(-100, 5), -95) + + def test_reverse(self): + self.assertEqual(reverse_list([1,2,3]), [3,2,1]) + +import cs101 +class Report1(Report): + title = "CS 101 Report 1" + questions = [(Week1, 10)] # Include a single question for 10 credits. + pack_imports = [cs101] + +if __name__ == "__main__": + # Uncomment to simply run everything as a unittest: + # unittest.main(verbosity=2) + evaluate_report_student(Report1()) +``` + +## Deployment +The above is all you need if you simply want to use the framework as a self-check: Students can run the code and see how well they did. +In order to begin using the framework for evaluation we need to create a bit more structure. We do that by deploying the report class as follows: +```python +from report1 import Report1 +from unitgrade_private2.hidden_create_files import setup_grade_file_report +from snipper import snip_dir +import shutil + +if __name__ == "__main__": + setup_grade_file_report(Report1, minify=False, obfuscate=False, execute=False) + + # Deploy the files using snipper: https://gitlab.compute.dtu.dk/tuhe/snipper + snip_dir.snip_dir(source_dir="../cs101", dest_dir="../../students/cs101", clean_destination_dir=True, exclude=['__pycache__', '*.token', 'deploy.py']) + ``` + - The first line creates the `report1_grade.py` script and any additional data files needed by the tests (none in this case) + - The second line set up the students directory (remember, we have included the solutions!) and remove the students solutions. You can check the results in the students folder. diff --git a/examples/example_docker/instructor/cs103/__pycache__/report3.cpython-38.pyc b/examples/example_docker/instructor/cs103/__pycache__/report3.cpython-38.pyc index 0edd5337296831e5e9223e6b60b503ad093cf02e..e5e928b3cf8c95f9ee7e0283457b713591a406e4 100644 GIT binary patch delta 19 acmZ3$v4Ddsl$V!_fq{WxU&2N%F%|$P_yflP delta 19 acmZ3$v4Ddsl$V!_fq{WxO8iDHF%|$O_XC^& diff --git a/examples/example_docker/instructor/cs103/deploy.py b/examples/example_docker/instructor/cs103/deploy.py index 807c37e..2429949 100644 --- a/examples/example_docker/instructor/cs103/deploy.py +++ b/examples/example_docker/instructor/cs103/deploy.py @@ -14,14 +14,10 @@ def deploy_student_files(): setup_grade_file_report(Report3, minify=False, obfuscate=False, execute=False) Report3.reset() - # from cs103.report3_complete import Report3 - # gather_upload_to_campusnet(Report3()) - fout, ReportWithoutHidden = remove_hidden_methods(Report3, outfile="report3.py") setup_grade_file_report(ReportWithoutHidden, minify=False, obfuscate=False, execute=False) sdir = "../../students/cs103" - snip_dir(source_dir="../cs103", dest_dir=sdir, clean_destination_dir=True, - exclude=['*.token', 'deploy.py', 'report3_complete*.py']) + snip_dir(source_dir="../cs103", dest_dir=sdir, clean_destination_dir=True, exclude=['__pycache__', '*.token', 'deploy.py', 'report3_complete*.py']) return sdir def run_student_code_on_docker(Dockerfile, student_token_file): @@ -36,13 +32,11 @@ def run_student_code_on_docker(Dockerfile, student_token_file): if __name__ == "__main__": # Step 1: Deploy the students files and return the directory they were written to student_directory = deploy_student_files() + # Step 2: Simulate that the student run their report script and generate a .token file. os.system("cd ../../students && python -m cs103.report3_grade") student_token_file = glob.glob(student_directory + "/*.token")[0] - # Let's quickly check the students score: - with open(student_token_file, 'rb') as f: - results = pickle.load(f) - print("Student's score was:", results['total']) + # Step 3: Compile the Docker image (obviously you will only do this once; add your packages to requirements.txt). Dockerfile = os.path.dirname(__file__) + "/../unitgrade-docker/Dockerfile" @@ -50,4 +44,9 @@ if __name__ == "__main__": # Step 4: Test the students .token file and get the results-token-file. Compare the contents with the students_token_file: checked_token = run_student_code_on_docker(Dockerfile, student_token_file) - print("My results of the students score was", checked_token['total']) + + # Let's quickly compare the students score to what we got (the dictionary contains all relevant information including code). + with open(student_token_file, 'rb') as f: + results = pickle.load(f) + print("Student's score was:", results['total']) + print("My independent evaluation of the students score was", checked_token['total']) diff --git a/examples/example_docker/instructor/cs103/report3_complete_grade.py b/examples/example_docker/instructor/cs103/report3_complete_grade.py index e7244c0..b0deb8e 100644 --- a/examples/example_docker/instructor/cs103/report3_complete_grade.py +++ b/examples/example_docker/instructor/cs103/report3_complete_grade.py @@ -429,7 +429,7 @@ def source_instantiate(name, report1_source, payload): report1_source = '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\nimport pickle\nimport itertools\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 weight = 1 # the weight of the question.\n\n def __init__(self, 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\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 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(f"Not equal within tolerance {tol=}; deviation was {np.max(diff)=}")\n print(f"Element-wise differences {diff.tolist()}")\n self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol=}, {np.max(diff)=}")\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 = "Untitled question"\n partially_scored = False\n t_init = 0 # Time spend on initialization (placeholder; set this externally).\n estimated_time = 0.42\n has_called_init_ = False\n _name = None\n _items = None\n\n @property\n def items(self):\n if self._items == 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 I in members:\n self._items.append( I(question=self))\n return self._items\n\n @items.setter\n def items(self, value):\n self._items = value\n\n @property\n def name(self):\n if self._name == None:\n self._name = self.__class__.__name__\n return self._name #\n\n @name.setter\n def name(self, val):\n self._name = val\n\n def init(self):\n # Can be used to set resources relevant for this question instance.\n pass\n\n def init_all_item_questions(self):\n for item in self.items:\n if not item.question.has_called_init_:\n item.question.init()\n item.question.has_called_init_ = True\n\n\nclass Report():\n title = "report title"\n version = None\n questions = []\n pack_imports = []\n individual_imports = []\n\n @classmethod\n def reset(cls):\n for (q,_) in cls.questions:\n if hasattr(q, \'reset\'):\n q.reset()\n\n def _file(self):\n return inspect.getfile(type(self))\n\n def __init__(self, strict=False, payload=None):\n working_directory = os.path.abspath(os.path.dirname(self._file()))\n\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\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 main(self, verbosity=1):\n # Run all tests using standard unittest (nothing fancy).\n import unittest\n loader = unittest.TestLoader()\n for q,_ in self.questions:\n import time\n start = time.time() # A good proxy for setup time is to\n suite = loader.loadTestsFromTestCase(q)\n unittest.TextTestRunner(verbosity=verbosity).run(suite)\n total = time.time() - start\n q.time = total\n\n def _setup_answers(self):\n self.main() # Run all tests in class just to get that out of the way...\n report_cache = {}\n for q, _ in self.questions:\n if hasattr(q, \'_save_cache\'):\n q()._save_cache()\n q._cache[\'time\'] = q.time\n report_cache[q.__qualname__] = q._cache\n else:\n report_cache[q.__qualname__] = {\'no cache see _setup_answers in unitgrade2.py\':True}\n return report_cache\n\n def set_payload(self, payloads, strict=False):\n for q, _ in self.questions:\n q._cache = payloads[q.__qualname__]\n\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)\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]: # can perhaps be removed later.\n # item.title = payloads[q.name][item.name][\'title\']\n # except Exception as e: # Cannot set attribute error. The title is a function (and probably should not be).\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 self.n = int(np.round(self.t / self.dt))\n # self.pbar = tqdm.tqdm(total=self.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 self.time_started = time.time()\n\n def terminate(self):\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 return time.time() - self.time_started\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\n\nfrom unittest.suite import _isnotsuite\n\nclass MySuite(unittest.suite.TestSuite): # Not sure we need this one anymore.\n pass\n\ndef instance_call_stack(instance):\n s = "-".join(map(lambda x: x.__name__, instance.__class__.mro()))\n return s\n\ndef get_class_that_defined_method(meth):\n for cls in inspect.getmro(meth.im_class):\n if meth.__name__ in cls.__dict__:\n return cls\n return None\n\ndef caller_name(skip=2):\n """Get a name of a caller in the format module.class.method\n\n `skip` specifies how many levels of stack to skip while getting caller\n name. skip=1 means "who calls me", skip=2 "who calls my caller" etc.\n\n An empty string is returned if skipped levels exceed stack height\n """\n stack = inspect.stack()\n start = 0 + skip\n if len(stack) < start + 1:\n return \'\'\n parentframe = stack[start][0]\n\n name = []\n module = inspect.getmodule(parentframe)\n # `modname` can be None when frame is executed directly in console\n # TODO(techtonik): consider using __main__\n if module:\n name.append(module.__name__)\n # detect classname\n if \'self\' in parentframe.f_locals:\n # I don\'t know any way to detect call from the object method\n # XXX: there seems to be no way to detect static method call - it will\n # be just a function call\n name.append(parentframe.f_locals[\'self\'].__class__.__name__)\n codename = parentframe.f_code.co_name\n if codename != \'<module>\': # top level usually\n name.append( codename ) # function or a method\n\n ## Avoid circular refs and frame leaks\n # https://docs.python.org/2.7/library/inspect.html#the-interpreter-stack\n del parentframe, stack\n\n return ".".join(name)\n\ndef get_class_from_frame(fr):\n import inspect\n args, _, _, value_dict = inspect.getargvalues(fr)\n # we check the first parameter for the frame function is\n # named \'self\'\n if len(args) and args[0] == \'self\':\n # in that case, \'self\' will be referenced in value_dict\n instance = value_dict.get(\'self\', None)\n if instance:\n # return its class\n # isinstance(instance, Testing) # is the actual class instance.\n\n return getattr(instance, \'__class__\', None)\n # return None otherwise\n return None\n\nfrom typing import Any\nimport inspect, gc\n\ndef giveupthefunc():\n frame = inspect.currentframe()\n code = frame.f_code\n globs = frame.f_globals\n functype = type(lambda: 0)\n funcs = []\n for func in gc.get_referrers(code):\n if type(func) is functype:\n if getattr(func, "__code__", None) is code:\n if getattr(func, "__globals__", None) is globs:\n funcs.append(func)\n if len(funcs) > 1:\n return None\n return funcs[0] if funcs else None\n\n\nfrom collections import defaultdict\n\nclass UTextResult(unittest.TextTestResult):\n def __init__(self, stream, descriptions, verbosity):\n super().__init__(stream, descriptions, verbosity)\n self.successes = []\n\n def printErrors(self) -> None:\n # if self.dots or self.showAll:\n # self.stream.writeln()\n self.printErrorList(\'ERROR\', self.errors)\n self.printErrorList(\'FAIL\', self.failures)\n\n\n def addSuccess(self, test: unittest.case.TestCase) -> None:\n # super().addSuccess(test)\n self.successes.append(test)\n # super().addSuccess(test)\n # hidden = issubclass(item.__class__, Hidden)\n # # if not hidden:\n # # print(ss, end="")\n # # sys.stdout.flush()\n # start = time.time()\n #\n # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)\n # q_[j] = {\'w\': item.weight, \'possible\': possible, \'obtained\': current, \'hidden\': hidden, \'computed\': str(item._computed_answer), \'title\': item.title}\n # tsecs = np.round(time.time()-start, 2)\n show_progress_bar = True\n nL = 80\n if show_progress_bar:\n tsecs = np.round( self.cc.terminate(), 2)\n sys.stdout.flush()\n ss = self.item_title_print\n print(self.item_title_print + (\'.\' * max(0, nL - 4 - len(ss))), end="")\n #\n # if not hidden:\n current = 1\n possible = 1\n # tsecs = 2\n ss = "PASS" if current == possible else "*** FAILED"\n if tsecs >= 0.1:\n ss += " ("+ str(tsecs) + " seconds)"\n print(ss)\n\n\n def startTest(self, test):\n # super().startTest(test)\n self.testsRun += 1\n # print("Starting the test...")\n show_progress_bar = True\n n = 1\n j = 1\n item_title = self.getDescription(test)\n item_title = item_title.split("\\n")[0]\n self.item_title_print = "*** q%i.%i) %s" % (n + 1, j + 1, item_title)\n estimated_time = 10\n nL = 80\n #\n if show_progress_bar:\n self.cc = ActiveProgress(t=estimated_time, title=self.item_title_print)\n else:\n print(self.item_title_print + (\'.\' * max(0, nL - 4 - len(self.item_title_print))), end="")\n\n self._test = test\n\n def _setupStdout(self):\n if self._previousTestClass == None:\n total_estimated_time = 2\n if hasattr(self.__class__, \'q_title_print\'):\n q_title_print = self.__class__.q_title_print\n else:\n q_title_print = "<unnamed test. See unitgrade.py>"\n\n # q_title_print = "some printed title..."\n cc = ActiveProgress(t=total_estimated_time, title=q_title_print)\n self.cc = cc\n\n def _restoreStdout(self): # Used when setting up the test.\n if self._previousTestClass == None:\n q_time = self.cc.terminate()\n q_time = np.round(q_time, 2)\n sys.stdout.flush()\n print(self.cc.title, end="")\n # start = 10\n # q_time = np.round(time.time() - start, 2)\n nL = 80\n print(" " * max(0, nL - len(self.cc.title)) + (\n " (" + str(q_time) + " seconds)" if q_time >= 0.1 else "")) # if q.name in report.payloads else "")\n print("=" * nL)\n\nfrom unittest.runner import _WritelnDecorator\nfrom io import StringIO\n\nclass UTextTestRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n from io import StringIO\n stream = StringIO()\n super().__init__(*args, stream=stream, **kwargs)\n\n def _makeResult(self):\n stream = self.stream # not you!\n stream = sys.stdout\n stream = _WritelnDecorator(stream)\n return self.resultclass(stream, self.descriptions, self.verbosity)\n\ndef wrapper(foo):\n def magic(self):\n s = "-".join(map(lambda x: x.__name__, self.__class__.mro()))\n # print(s)\n foo(self)\n magic.__doc__ = foo.__doc__\n return magic\n\nfrom functools import update_wrapper, _make_key, RLock\nfrom collections import namedtuple\n_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])\n\ndef cache(foo, typed=False):\n """ Magic cache wrapper\n https://github.com/python/cpython/blob/main/Lib/functools.py\n """\n maxsize = None\n def wrapper(self, *args, **kwargs):\n key = self.cache_id() + ("cache", _make_key(args, kwargs, typed))\n if not self._cache_contains(key):\n value = foo(self, *args, **kwargs)\n self._cache_put(key, value)\n else:\n value = self._cache_get(key)\n return value\n return wrapper\n\n\nclass UTestCase(unittest.TestCase):\n _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache.\n _cache = None # Read-only cache.\n _cache2 = None # User-written cache\n\n @classmethod\n def reset(cls):\n cls._outcome = None\n cls._cache = None\n cls._cache2 = None\n\n def _get_outcome(self):\n if not (self.__class__, \'_outcome\') or self.__class__._outcome == None:\n self.__class__._outcome = {}\n return self.__class__._outcome\n\n def _callTestMethod(self, testMethod):\n t = time.time()\n res = testMethod()\n elapsed = time.time() - t\n # if res == None:\n # res = {}\n # res[\'time\'] = elapsed\n sd = self.shortDescription()\n self._cache_put( (self.cache_id(), \'title\'), self._testMethodName if sd == None else sd)\n # self._test_fun_output = res\n self._get_outcome()[self.cache_id()] = res\n self._cache_put( (self.cache_id(), "time"), elapsed)\n\n\n # This is my base test class. So what is new about it?\n def cache_id(self):\n c = self.__class__.__qualname__\n m = self._testMethodName\n return (c,m)\n\n def unique_cache_id(self):\n k0 = self.cache_id()\n key = ()\n for i in itertools.count():\n key = k0 + (i,)\n if not self._cache2_contains(key):\n break\n return key\n\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self._load_cache()\n self.cache_indexes = defaultdict(lambda: 0)\n\n def _ensure_cache_exists(self):\n if not hasattr(self.__class__, \'_cache\') or self.__class__._cache == None:\n self.__class__._cache = dict()\n if not hasattr(self.__class__, \'_cache2\') or self.__class__._cache2 == None:\n self.__class__._cache2 = dict()\n\n def _cache_get(self, key, default=None):\n self._ensure_cache_exists()\n return self.__class__._cache.get(key, default)\n\n def _cache_put(self, key, value):\n self._ensure_cache_exists()\n self.__class__._cache2[key] = value\n\n def _cache_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache\n\n def _cache2_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache2\n\n def assertEqualC(self, first: Any, msg: Any = ...) -> None:\n id = self.unique_cache_id()\n if not self._cache_contains(id):\n print("Warning, framework missing key", id)\n\n self.assertEqual(first, self._cache_get(id, first), msg)\n self._cache_put(id, first)\n\n def _cache_file(self):\n return os.path.dirname(inspect.getfile(self.__class__) ) + "/unitgrade/" + self.__class__.__name__ + ".pkl"\n\n def _save_cache(self):\n # get the class name (i.e. what to save to).\n cfile = self._cache_file()\n if not os.path.isdir(os.path.dirname(cfile)):\n os.makedirs(os.path.dirname(cfile))\n\n if hasattr(self.__class__, \'_cache2\'):\n with open(cfile, \'wb\') as f:\n pickle.dump(self.__class__._cache2, f)\n\n # But you can also set cache explicitly.\n def _load_cache(self):\n if self._cache != None: # Cache already loaded. We will not load it twice.\n return\n # raise Exception("Loaded cache which was already set. What is going on?!")\n cfile = self._cache_file()\n print("Loading cache from", cfile)\n if os.path.exists(cfile):\n with open(cfile, \'rb\') as f:\n data = pickle.load(f)\n self.__class__._cache = data\n else:\n print("Warning! data file not found", cfile)\n\ndef hide(func):\n return func\n\ndef makeRegisteringDecorator(foreignDecorator):\n """\n Returns a copy of foreignDecorator, which is identical in every\n way(*), except also appends a .decorator property to the callable it\n spits out.\n """\n def newDecorator(func):\n # Call to newDecorator(method)\n # Exactly like old decorator, but output keeps track of what decorated it\n R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done\n R.decorator = newDecorator # keep track of decorator\n # R.original = func # might as well keep track of everything!\n return R\n\n newDecorator.__name__ = foreignDecorator.__name__\n newDecorator.__doc__ = foreignDecorator.__doc__\n # (*)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\n return newDecorator\n\nhide = makeRegisteringDecorator(hide)\n\ndef methodsWithDecorator(cls, decorator):\n """\n Returns all methods in CLS with DECORATOR as the\n outermost decorator.\n\n DECORATOR must be a "registering decorator"; one\n can make any decorator "registering" via the\n makeRegisteringDecorator function.\n\n import inspect\n ls = list(methodsWithDecorator(GeneratorQuestion, deco))\n for f in ls:\n print(inspect.getsourcelines(f) ) # How to get all hidden questions.\n """\n for maybeDecorated in cls.__dict__.values():\n if hasattr(maybeDecorated, \'decorator\'):\n if maybeDecorated.decorator == decorator:\n print(maybeDecorated)\n yield maybeDecorated\n\n\n\nimport numpy as np\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport pyfiglet\nimport unittest\n\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\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\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 hasattr(report, "computed_answer_file") and 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\n # try: # For registering stats.\n # import unitgrade_private\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\n\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\nclass UnitgradeTextRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\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 hasattr(report, "version") and 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\n # Use the sequential test loader instead. See here:\n class SequentialTestLoader(unittest.TestLoader):\n def getTestCaseNames(self, testCaseClass):\n test_names = super().getTestCaseNames(testCaseClass)\n testcase_methods = list(testCaseClass.__dict__.keys())\n test_names.sort(key=testcase_methods.index)\n return test_names\n loader = SequentialTestLoader()\n # loader = unittest.TestLoader()\n # loader.suiteClass = MySuite\n\n for n, (q, w) in enumerate(report.questions):\n # q = q()\n q_hidden = False\n # q_hidden = issubclass(q.__class__, Hidden)\n if question is not None and n+1 != question:\n continue\n suite = loader.loadTestsFromTestCase(q)\n # print(suite)\n qtitle = q.__name__\n # qtitle = q.title if hasattr(q, "title") else q.id()\n # q.title = qtitle\n q_title_print = "Question %i: %s"%(n+1, qtitle)\n print(q_title_print, end="")\n q.possible = 0\n q.obtained = 0\n q_ = {} # Gather score in this class.\n # unittest.Te\n # q_with_outstanding_init = [item.question for item in q.items if not item.question.has_called_init_]\n UTextResult.q_title_print = q_title_print # Hacky\n res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite)\n # res = UTextTestRunner(verbosity=2, resultclass=unittest.TextTestResult).run(suite)\n z = 234\n # for j, item in enumerate(q.items):\n # if qitem is not None and question is not None and j+1 != qitem:\n # continue\n #\n # if q_with_outstanding_init is not None: # check for None bc. this must be called to set titles.\n # # if not item.question.has_called_init_:\n # start = time.time()\n #\n # cc = None\n # if show_progress_bar:\n # 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] )\n # cc = ActiveProgress(t=total_estimated_time, title=q_title_print)\n # from unitgrade import Capturing # DON\'T REMOVE THIS LINE\n # with eval(\'Capturing\')(unmute=unmute): # Clunky import syntax is required bc. of minify issue.\n # try:\n # for q2 in q_with_outstanding_init:\n # q2.init()\n # q2.has_called_init_ = True\n #\n # # item.question.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_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 # q_with_outstanding_init = None\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 #\n # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)\n # q_[j] = {\'w\': item.weight, \'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\n possible = res.testsRun\n obtained = possible - len(res.errors)\n\n\n # possible = int(ws @ possible)\n # obtained = int(ws @ obtained)\n # obtained = int(myround(int((w * obtained) / possible ))) if possible > 0 else 0\n\n obtained = w * int(obtained * 1.0 / possible )\n score[n] = {\'w\': w, \'possible\': w, \'obtained\': obtained, \'items\': q_, \'title\': qtitle}\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\n\n\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport inspect\nimport json\nimport os\nimport bz2\nimport pickle\nimport os\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 = str(__import__(m.__name__.split(\'.\')[0]).__path__)\n if m.__class__.__name__ == \'module\' and False:\n top_package = os.path.dirname(m.__file__)\n module_import = True\n else:\n top_package = __import__(m.__name__.split(\'.\')[0]).__path__._path[0]\n module_import = False\n\n # top_package = os.path.dirname(__import__(m.__name__.split(\'.\')[0]).__file__)\n # top_package = os.path.dirname(top_package)\n import zipfile\n # import strea\n # zipfile.ZipFile\n import io\n # file_like_object = io.BytesIO(my_zip_data)\n zip_buffer = io.BytesIO()\n with zipfile.ZipFile(zip_buffer, \'w\') as zip:\n # zip.write()\n for root, dirs, files in os.walk(top_package):\n for file in files:\n if file.endswith(".py"):\n fpath = os.path.join(root, file)\n v = os.path.relpath(os.path.join(root, file), os.path.dirname(top_package))\n zip.write(fpath, v)\n\n resources[\'zipfile\'] = zip_buffer.getvalue()\n resources[\'top_package\'] = top_package\n resources[\'module_import\'] = module_import\n return resources, 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 k, m in enumerate(report.pack_imports):\n nimp, top_package = gather_imports(m)\n report_relative_location = os.path.relpath(inspect.getfile(report.__class__), top_package)\n nimp[\'report_relative_location\'] = report_relative_location\n nimp[\'name\'] = m.__name__\n sources[k] = nimp\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 with open(token, \'wb\') as f:\n pickle.dump(results, f)\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.9.0"\n\n\nclass Week1(UTestCase):\n """ The first question for week 1. """\n def test_add(self):\n from cs103.homework1 import add\n self.assertEqualC(add(2,2))\n self.assertEqualC(add(-100, 5))\n\n @hide\n def test_add_hidden(self):\n # This is a hidden test. The @hide-decorator will allow unitgrade to remove the test.\n # See the output in the student directory for more information.\n from cs103.homework1 import add\n self.assertEqualC(add(2,2))\n\nimport cs103\nclass Report3(Report):\n title = "CS 101 Report 3"\n questions = [(Week1, 20)] # Include a single question for 10 credits.\n pack_imports = [cs103]' -report1_payload = '80049570000000000000007d948c055765656b31947d94288c055765656b31948c08746573745f616464944b0087944b04680368044b0187944aa1ffffff6803680486948c057469746c6594869468046803680486948c0474696d659486944700000000000000008c0474696d6594473f505b000000000075732e' +report1_payload = '80049570000000000000007d948c055765656b31947d94288c055765656b31948c08746573745f616464944b0087944b04680368044b0187944aa1ffffff6803680486948c057469746c6594869468046803680486948c0474696d659486944700000000000000008c0474696d6594473f6066800000000075732e' name="Report3" report = source_instantiate(name, report1_source, report1_payload) diff --git a/examples/example_docker/instructor/cs103/report3_grade.py b/examples/example_docker/instructor/cs103/report3_grade.py index 8d21569..7a98ca2 100644 --- a/examples/example_docker/instructor/cs103/report3_grade.py +++ b/examples/example_docker/instructor/cs103/report3_grade.py @@ -429,7 +429,7 @@ def source_instantiate(name, report1_source, payload): report1_source = '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\nimport pickle\nimport itertools\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 weight = 1 # the weight of the question.\n\n def __init__(self, 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\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 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(f"Not equal within tolerance {tol=}; deviation was {np.max(diff)=}")\n print(f"Element-wise differences {diff.tolist()}")\n self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol=}, {np.max(diff)=}")\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 = "Untitled question"\n partially_scored = False\n t_init = 0 # Time spend on initialization (placeholder; set this externally).\n estimated_time = 0.42\n has_called_init_ = False\n _name = None\n _items = None\n\n @property\n def items(self):\n if self._items == 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 I in members:\n self._items.append( I(question=self))\n return self._items\n\n @items.setter\n def items(self, value):\n self._items = value\n\n @property\n def name(self):\n if self._name == None:\n self._name = self.__class__.__name__\n return self._name #\n\n @name.setter\n def name(self, val):\n self._name = val\n\n def init(self):\n # Can be used to set resources relevant for this question instance.\n pass\n\n def init_all_item_questions(self):\n for item in self.items:\n if not item.question.has_called_init_:\n item.question.init()\n item.question.has_called_init_ = True\n\n\nclass Report():\n title = "report title"\n version = None\n questions = []\n pack_imports = []\n individual_imports = []\n\n @classmethod\n def reset(cls):\n for (q,_) in cls.questions:\n if hasattr(q, \'reset\'):\n q.reset()\n\n def _file(self):\n return inspect.getfile(type(self))\n\n def __init__(self, strict=False, payload=None):\n working_directory = os.path.abspath(os.path.dirname(self._file()))\n\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\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 main(self, verbosity=1):\n # Run all tests using standard unittest (nothing fancy).\n import unittest\n loader = unittest.TestLoader()\n for q,_ in self.questions:\n import time\n start = time.time() # A good proxy for setup time is to\n suite = loader.loadTestsFromTestCase(q)\n unittest.TextTestRunner(verbosity=verbosity).run(suite)\n total = time.time() - start\n q.time = total\n\n def _setup_answers(self):\n self.main() # Run all tests in class just to get that out of the way...\n report_cache = {}\n for q, _ in self.questions:\n if hasattr(q, \'_save_cache\'):\n q()._save_cache()\n q._cache[\'time\'] = q.time\n report_cache[q.__qualname__] = q._cache\n else:\n report_cache[q.__qualname__] = {\'no cache see _setup_answers in unitgrade2.py\':True}\n return report_cache\n\n def set_payload(self, payloads, strict=False):\n for q, _ in self.questions:\n q._cache = payloads[q.__qualname__]\n\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)\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]: # can perhaps be removed later.\n # item.title = payloads[q.name][item.name][\'title\']\n # except Exception as e: # Cannot set attribute error. The title is a function (and probably should not be).\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 self.n = int(np.round(self.t / self.dt))\n # self.pbar = tqdm.tqdm(total=self.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 self.time_started = time.time()\n\n def terminate(self):\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 return time.time() - self.time_started\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\n\nfrom unittest.suite import _isnotsuite\n\nclass MySuite(unittest.suite.TestSuite): # Not sure we need this one anymore.\n pass\n\ndef instance_call_stack(instance):\n s = "-".join(map(lambda x: x.__name__, instance.__class__.mro()))\n return s\n\ndef get_class_that_defined_method(meth):\n for cls in inspect.getmro(meth.im_class):\n if meth.__name__ in cls.__dict__:\n return cls\n return None\n\ndef caller_name(skip=2):\n """Get a name of a caller in the format module.class.method\n\n `skip` specifies how many levels of stack to skip while getting caller\n name. skip=1 means "who calls me", skip=2 "who calls my caller" etc.\n\n An empty string is returned if skipped levels exceed stack height\n """\n stack = inspect.stack()\n start = 0 + skip\n if len(stack) < start + 1:\n return \'\'\n parentframe = stack[start][0]\n\n name = []\n module = inspect.getmodule(parentframe)\n # `modname` can be None when frame is executed directly in console\n # TODO(techtonik): consider using __main__\n if module:\n name.append(module.__name__)\n # detect classname\n if \'self\' in parentframe.f_locals:\n # I don\'t know any way to detect call from the object method\n # XXX: there seems to be no way to detect static method call - it will\n # be just a function call\n name.append(parentframe.f_locals[\'self\'].__class__.__name__)\n codename = parentframe.f_code.co_name\n if codename != \'<module>\': # top level usually\n name.append( codename ) # function or a method\n\n ## Avoid circular refs and frame leaks\n # https://docs.python.org/2.7/library/inspect.html#the-interpreter-stack\n del parentframe, stack\n\n return ".".join(name)\n\ndef get_class_from_frame(fr):\n import inspect\n args, _, _, value_dict = inspect.getargvalues(fr)\n # we check the first parameter for the frame function is\n # named \'self\'\n if len(args) and args[0] == \'self\':\n # in that case, \'self\' will be referenced in value_dict\n instance = value_dict.get(\'self\', None)\n if instance:\n # return its class\n # isinstance(instance, Testing) # is the actual class instance.\n\n return getattr(instance, \'__class__\', None)\n # return None otherwise\n return None\n\nfrom typing import Any\nimport inspect, gc\n\ndef giveupthefunc():\n frame = inspect.currentframe()\n code = frame.f_code\n globs = frame.f_globals\n functype = type(lambda: 0)\n funcs = []\n for func in gc.get_referrers(code):\n if type(func) is functype:\n if getattr(func, "__code__", None) is code:\n if getattr(func, "__globals__", None) is globs:\n funcs.append(func)\n if len(funcs) > 1:\n return None\n return funcs[0] if funcs else None\n\n\nfrom collections import defaultdict\n\nclass UTextResult(unittest.TextTestResult):\n def __init__(self, stream, descriptions, verbosity):\n super().__init__(stream, descriptions, verbosity)\n self.successes = []\n\n def printErrors(self) -> None:\n # if self.dots or self.showAll:\n # self.stream.writeln()\n self.printErrorList(\'ERROR\', self.errors)\n self.printErrorList(\'FAIL\', self.failures)\n\n\n def addSuccess(self, test: unittest.case.TestCase) -> None:\n # super().addSuccess(test)\n self.successes.append(test)\n # super().addSuccess(test)\n # hidden = issubclass(item.__class__, Hidden)\n # # if not hidden:\n # # print(ss, end="")\n # # sys.stdout.flush()\n # start = time.time()\n #\n # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)\n # q_[j] = {\'w\': item.weight, \'possible\': possible, \'obtained\': current, \'hidden\': hidden, \'computed\': str(item._computed_answer), \'title\': item.title}\n # tsecs = np.round(time.time()-start, 2)\n show_progress_bar = True\n nL = 80\n if show_progress_bar:\n tsecs = np.round( self.cc.terminate(), 2)\n sys.stdout.flush()\n ss = self.item_title_print\n print(self.item_title_print + (\'.\' * max(0, nL - 4 - len(ss))), end="")\n #\n # if not hidden:\n current = 1\n possible = 1\n # tsecs = 2\n ss = "PASS" if current == possible else "*** FAILED"\n if tsecs >= 0.1:\n ss += " ("+ str(tsecs) + " seconds)"\n print(ss)\n\n\n def startTest(self, test):\n # super().startTest(test)\n self.testsRun += 1\n # print("Starting the test...")\n show_progress_bar = True\n n = 1\n j = 1\n item_title = self.getDescription(test)\n item_title = item_title.split("\\n")[0]\n self.item_title_print = "*** q%i.%i) %s" % (n + 1, j + 1, item_title)\n estimated_time = 10\n nL = 80\n #\n if show_progress_bar:\n self.cc = ActiveProgress(t=estimated_time, title=self.item_title_print)\n else:\n print(self.item_title_print + (\'.\' * max(0, nL - 4 - len(self.item_title_print))), end="")\n\n self._test = test\n\n def _setupStdout(self):\n if self._previousTestClass == None:\n total_estimated_time = 2\n if hasattr(self.__class__, \'q_title_print\'):\n q_title_print = self.__class__.q_title_print\n else:\n q_title_print = "<unnamed test. See unitgrade.py>"\n\n # q_title_print = "some printed title..."\n cc = ActiveProgress(t=total_estimated_time, title=q_title_print)\n self.cc = cc\n\n def _restoreStdout(self): # Used when setting up the test.\n if self._previousTestClass == None:\n q_time = self.cc.terminate()\n q_time = np.round(q_time, 2)\n sys.stdout.flush()\n print(self.cc.title, end="")\n # start = 10\n # q_time = np.round(time.time() - start, 2)\n nL = 80\n print(" " * max(0, nL - len(self.cc.title)) + (\n " (" + str(q_time) + " seconds)" if q_time >= 0.1 else "")) # if q.name in report.payloads else "")\n print("=" * nL)\n\nfrom unittest.runner import _WritelnDecorator\nfrom io import StringIO\n\nclass UTextTestRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n from io import StringIO\n stream = StringIO()\n super().__init__(*args, stream=stream, **kwargs)\n\n def _makeResult(self):\n stream = self.stream # not you!\n stream = sys.stdout\n stream = _WritelnDecorator(stream)\n return self.resultclass(stream, self.descriptions, self.verbosity)\n\ndef wrapper(foo):\n def magic(self):\n s = "-".join(map(lambda x: x.__name__, self.__class__.mro()))\n # print(s)\n foo(self)\n magic.__doc__ = foo.__doc__\n return magic\n\nfrom functools import update_wrapper, _make_key, RLock\nfrom collections import namedtuple\n_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])\n\ndef cache(foo, typed=False):\n """ Magic cache wrapper\n https://github.com/python/cpython/blob/main/Lib/functools.py\n """\n maxsize = None\n def wrapper(self, *args, **kwargs):\n key = self.cache_id() + ("cache", _make_key(args, kwargs, typed))\n if not self._cache_contains(key):\n value = foo(self, *args, **kwargs)\n self._cache_put(key, value)\n else:\n value = self._cache_get(key)\n return value\n return wrapper\n\n\nclass UTestCase(unittest.TestCase):\n _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache.\n _cache = None # Read-only cache.\n _cache2 = None # User-written cache\n\n @classmethod\n def reset(cls):\n cls._outcome = None\n cls._cache = None\n cls._cache2 = None\n\n def _get_outcome(self):\n if not (self.__class__, \'_outcome\') or self.__class__._outcome == None:\n self.__class__._outcome = {}\n return self.__class__._outcome\n\n def _callTestMethod(self, testMethod):\n t = time.time()\n res = testMethod()\n elapsed = time.time() - t\n # if res == None:\n # res = {}\n # res[\'time\'] = elapsed\n sd = self.shortDescription()\n self._cache_put( (self.cache_id(), \'title\'), self._testMethodName if sd == None else sd)\n # self._test_fun_output = res\n self._get_outcome()[self.cache_id()] = res\n self._cache_put( (self.cache_id(), "time"), elapsed)\n\n\n # This is my base test class. So what is new about it?\n def cache_id(self):\n c = self.__class__.__qualname__\n m = self._testMethodName\n return (c,m)\n\n def unique_cache_id(self):\n k0 = self.cache_id()\n key = ()\n for i in itertools.count():\n key = k0 + (i,)\n if not self._cache2_contains(key):\n break\n return key\n\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self._load_cache()\n self.cache_indexes = defaultdict(lambda: 0)\n\n def _ensure_cache_exists(self):\n if not hasattr(self.__class__, \'_cache\') or self.__class__._cache == None:\n self.__class__._cache = dict()\n if not hasattr(self.__class__, \'_cache2\') or self.__class__._cache2 == None:\n self.__class__._cache2 = dict()\n\n def _cache_get(self, key, default=None):\n self._ensure_cache_exists()\n return self.__class__._cache.get(key, default)\n\n def _cache_put(self, key, value):\n self._ensure_cache_exists()\n self.__class__._cache2[key] = value\n\n def _cache_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache\n\n def _cache2_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache2\n\n def assertEqualC(self, first: Any, msg: Any = ...) -> None:\n id = self.unique_cache_id()\n if not self._cache_contains(id):\n print("Warning, framework missing key", id)\n\n self.assertEqual(first, self._cache_get(id, first), msg)\n self._cache_put(id, first)\n\n def _cache_file(self):\n return os.path.dirname(inspect.getfile(self.__class__) ) + "/unitgrade/" + self.__class__.__name__ + ".pkl"\n\n def _save_cache(self):\n # get the class name (i.e. what to save to).\n cfile = self._cache_file()\n if not os.path.isdir(os.path.dirname(cfile)):\n os.makedirs(os.path.dirname(cfile))\n\n if hasattr(self.__class__, \'_cache2\'):\n with open(cfile, \'wb\') as f:\n pickle.dump(self.__class__._cache2, f)\n\n # But you can also set cache explicitly.\n def _load_cache(self):\n if self._cache != None: # Cache already loaded. We will not load it twice.\n return\n # raise Exception("Loaded cache which was already set. What is going on?!")\n cfile = self._cache_file()\n print("Loading cache from", cfile)\n if os.path.exists(cfile):\n with open(cfile, \'rb\') as f:\n data = pickle.load(f)\n self.__class__._cache = data\n else:\n print("Warning! data file not found", cfile)\n\ndef hide(func):\n return func\n\ndef makeRegisteringDecorator(foreignDecorator):\n """\n Returns a copy of foreignDecorator, which is identical in every\n way(*), except also appends a .decorator property to the callable it\n spits out.\n """\n def newDecorator(func):\n # Call to newDecorator(method)\n # Exactly like old decorator, but output keeps track of what decorated it\n R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done\n R.decorator = newDecorator # keep track of decorator\n # R.original = func # might as well keep track of everything!\n return R\n\n newDecorator.__name__ = foreignDecorator.__name__\n newDecorator.__doc__ = foreignDecorator.__doc__\n # (*)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\n return newDecorator\n\nhide = makeRegisteringDecorator(hide)\n\ndef methodsWithDecorator(cls, decorator):\n """\n Returns all methods in CLS with DECORATOR as the\n outermost decorator.\n\n DECORATOR must be a "registering decorator"; one\n can make any decorator "registering" via the\n makeRegisteringDecorator function.\n\n import inspect\n ls = list(methodsWithDecorator(GeneratorQuestion, deco))\n for f in ls:\n print(inspect.getsourcelines(f) ) # How to get all hidden questions.\n """\n for maybeDecorated in cls.__dict__.values():\n if hasattr(maybeDecorated, \'decorator\'):\n if maybeDecorated.decorator == decorator:\n print(maybeDecorated)\n yield maybeDecorated\n\n\n\nimport numpy as np\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport pyfiglet\nimport unittest\n\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\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\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 hasattr(report, "computed_answer_file") and 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\n # try: # For registering stats.\n # import unitgrade_private\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\n\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\nclass UnitgradeTextRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\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 hasattr(report, "version") and 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\n # Use the sequential test loader instead. See here:\n class SequentialTestLoader(unittest.TestLoader):\n def getTestCaseNames(self, testCaseClass):\n test_names = super().getTestCaseNames(testCaseClass)\n testcase_methods = list(testCaseClass.__dict__.keys())\n test_names.sort(key=testcase_methods.index)\n return test_names\n loader = SequentialTestLoader()\n # loader = unittest.TestLoader()\n # loader.suiteClass = MySuite\n\n for n, (q, w) in enumerate(report.questions):\n # q = q()\n q_hidden = False\n # q_hidden = issubclass(q.__class__, Hidden)\n if question is not None and n+1 != question:\n continue\n suite = loader.loadTestsFromTestCase(q)\n # print(suite)\n qtitle = q.__name__\n # qtitle = q.title if hasattr(q, "title") else q.id()\n # q.title = qtitle\n q_title_print = "Question %i: %s"%(n+1, qtitle)\n print(q_title_print, end="")\n q.possible = 0\n q.obtained = 0\n q_ = {} # Gather score in this class.\n # unittest.Te\n # q_with_outstanding_init = [item.question for item in q.items if not item.question.has_called_init_]\n UTextResult.q_title_print = q_title_print # Hacky\n res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite)\n # res = UTextTestRunner(verbosity=2, resultclass=unittest.TextTestResult).run(suite)\n z = 234\n # for j, item in enumerate(q.items):\n # if qitem is not None and question is not None and j+1 != qitem:\n # continue\n #\n # if q_with_outstanding_init is not None: # check for None bc. this must be called to set titles.\n # # if not item.question.has_called_init_:\n # start = time.time()\n #\n # cc = None\n # if show_progress_bar:\n # 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] )\n # cc = ActiveProgress(t=total_estimated_time, title=q_title_print)\n # from unitgrade import Capturing # DON\'T REMOVE THIS LINE\n # with eval(\'Capturing\')(unmute=unmute): # Clunky import syntax is required bc. of minify issue.\n # try:\n # for q2 in q_with_outstanding_init:\n # q2.init()\n # q2.has_called_init_ = True\n #\n # # item.question.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_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 # q_with_outstanding_init = None\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 #\n # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)\n # q_[j] = {\'w\': item.weight, \'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\n possible = res.testsRun\n obtained = possible - len(res.errors)\n\n\n # possible = int(ws @ possible)\n # obtained = int(ws @ obtained)\n # obtained = int(myround(int((w * obtained) / possible ))) if possible > 0 else 0\n\n obtained = w * int(obtained * 1.0 / possible )\n score[n] = {\'w\': w, \'possible\': w, \'obtained\': obtained, \'items\': q_, \'title\': qtitle}\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\n\n\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport inspect\nimport json\nimport os\nimport bz2\nimport pickle\nimport os\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 = str(__import__(m.__name__.split(\'.\')[0]).__path__)\n if m.__class__.__name__ == \'module\' and False:\n top_package = os.path.dirname(m.__file__)\n module_import = True\n else:\n top_package = __import__(m.__name__.split(\'.\')[0]).__path__._path[0]\n module_import = False\n\n # top_package = os.path.dirname(__import__(m.__name__.split(\'.\')[0]).__file__)\n # top_package = os.path.dirname(top_package)\n import zipfile\n # import strea\n # zipfile.ZipFile\n import io\n # file_like_object = io.BytesIO(my_zip_data)\n zip_buffer = io.BytesIO()\n with zipfile.ZipFile(zip_buffer, \'w\') as zip:\n # zip.write()\n for root, dirs, files in os.walk(top_package):\n for file in files:\n if file.endswith(".py"):\n fpath = os.path.join(root, file)\n v = os.path.relpath(os.path.join(root, file), os.path.dirname(top_package))\n zip.write(fpath, v)\n\n resources[\'zipfile\'] = zip_buffer.getvalue()\n resources[\'top_package\'] = top_package\n resources[\'module_import\'] = module_import\n return resources, 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 k, m in enumerate(report.pack_imports):\n nimp, top_package = gather_imports(m)\n report_relative_location = os.path.relpath(inspect.getfile(report.__class__), top_package)\n nimp[\'report_relative_location\'] = report_relative_location\n nimp[\'name\'] = m.__name__\n sources[k] = nimp\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 with open(token, \'wb\') as f:\n pickle.dump(results, f)\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.9.0"\n\n\nclass Week1(UTestCase):\n """ The first question for week 1. """\n def test_add(self):\n from cs103.homework1 import add\n self.assertEqualC(add(2,2))\n self.assertEqualC(add(-100, 5))\n\n\nimport cs103\nclass Report3(Report):\n title = "CS 101 Report 3"\n questions = [(Week1, 20)] # Include a single question for 10 credits.\n pack_imports = [cs103]' -report1_payload = '800495a9000000000000007d948c055765656b31947d94288c055765656b31948c08746573745f616464944b0087944b04680368044b0187944aa1ffffff6803680486948c057469746c6594869468046803680486948c0474696d65948694473f505b000000000068038c0f746573745f6164645f68696464656e944b0087944b046803680d869468088694680d6803680d8694680b86944700000000000000008c0474696d6594473f805fc00000000075732e' +report1_payload = '800495a9000000000000007d948c055765656b31947d94288c055765656b31948c08746573745f616464944b0087944b04680368044b0187944aa1ffffff6803680486948c057469746c6594869468046803680486948c0474696d65948694473f5061000000000068038c0f746573745f6164645f68696464656e944b0087944b046803680d869468088694680d6803680d8694680b86944700000000000000008c0474696d6594473f7ca5000000000075732e' name="Report3" report = source_instantiate(name, report1_source, report1_payload) diff --git a/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/Report3_handin_0_of_20.token b/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/Report3_handin_0_of_20.token index 09b7d0966cea33dd01451ccee5e5befd524b1dd7..f341fcbe61424bb2cfb1ff275aada4c9ac0b1d87 100644 GIT binary patch delta 142 zcmeydo#V%LjtP?)S8be<%FMWG^DpLCOdP9xq=Jv0S?<yNhHd*BHb!nvRx<-LGmGhe z_!&=tB_}o*UuZugz_|U40Fx01lbPW(Nv1v)^W;R+>A(3I_w(~|1$Z+ui7<;WfWWHh i2~te<jH{+EkYch0>jxP={izg_E0_l|ZMu;((=h<28!*uT delta 143 zcmeydo#V%LjtP?)c{ff;WoG2v{EPV&69=!4RPY%wdGY2qY}?<kF>-UVni`lU8BG7f z&v*hXnY(%Mnf5aRjN8u$Fd1<$nI=t>Wa?wJFfdI^1}WOd&&w6y&B!FeEW!W+ywek; gnCuyOr!SCVvIQFeGJX0}DJEAi4`keQBWb2%0PjXBm;e9( diff --git a/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/Report3_handin_20_of_20.token b/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/Report3_handin_20_of_20.token deleted file mode 100644 index a402e6183235eee92b6de0f54d950df094897ccc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138309 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 ABme*a diff --git a/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/__pycache__/homework1.cpython-38.pyc b/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/__pycache__/homework1.cpython-38.pyc index 1f10f66e9b20dea24d1969426da24f2459b0e1f8..de977681ea2f743536e28e736d54eac5477fa5a8 100644 GIT binary patch delta 19 acmbQmK8u|zl$V!_fq{YHV!}qQ3CsW}V+2J2 delta 19 acmbQmK8u|zl$V!_fq{Wxb^J!I3CsW|dITN- diff --git a/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/__pycache__/report3_complete_grade.cpython-38.pyc b/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/__pycache__/report3_complete_grade.cpython-38.pyc index 870a12277bd7eac55d2382ab61dbf6bf9b568faa..324b033b154a5d1db71862ebf7103810d4499aa2 100644 GIT binary patch delta 32 ocmdmggn9oFX0A|PUM>a(28N3X8@Wmzu$mc|nOSUZez1oT0I6FFvH$=8 delta 32 ocmdmggn9oFX0A|PUM>a(28Pw~8@Wmzu$mf}CK+sQez1oT0H?PKtpET3 diff --git a/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/__pycache__/report3_grade.cpython-38.pyc b/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/__pycache__/report3_grade.cpython-38.pyc deleted file mode 100644 index daea90bc8b208769b148b93df011cd59b9a5460e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57802 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 DO%MeQ diff --git a/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/report3_complete_grade.py b/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/report3_complete_grade.py index e7244c0..b0deb8e 100644 --- a/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/report3_complete_grade.py +++ b/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/report3_complete_grade.py @@ -429,7 +429,7 @@ def source_instantiate(name, report1_source, payload): report1_source = '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\nimport pickle\nimport itertools\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 weight = 1 # the weight of the question.\n\n def __init__(self, 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\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 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(f"Not equal within tolerance {tol=}; deviation was {np.max(diff)=}")\n print(f"Element-wise differences {diff.tolist()}")\n self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol=}, {np.max(diff)=}")\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 = "Untitled question"\n partially_scored = False\n t_init = 0 # Time spend on initialization (placeholder; set this externally).\n estimated_time = 0.42\n has_called_init_ = False\n _name = None\n _items = None\n\n @property\n def items(self):\n if self._items == 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 I in members:\n self._items.append( I(question=self))\n return self._items\n\n @items.setter\n def items(self, value):\n self._items = value\n\n @property\n def name(self):\n if self._name == None:\n self._name = self.__class__.__name__\n return self._name #\n\n @name.setter\n def name(self, val):\n self._name = val\n\n def init(self):\n # Can be used to set resources relevant for this question instance.\n pass\n\n def init_all_item_questions(self):\n for item in self.items:\n if not item.question.has_called_init_:\n item.question.init()\n item.question.has_called_init_ = True\n\n\nclass Report():\n title = "report title"\n version = None\n questions = []\n pack_imports = []\n individual_imports = []\n\n @classmethod\n def reset(cls):\n for (q,_) in cls.questions:\n if hasattr(q, \'reset\'):\n q.reset()\n\n def _file(self):\n return inspect.getfile(type(self))\n\n def __init__(self, strict=False, payload=None):\n working_directory = os.path.abspath(os.path.dirname(self._file()))\n\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\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 main(self, verbosity=1):\n # Run all tests using standard unittest (nothing fancy).\n import unittest\n loader = unittest.TestLoader()\n for q,_ in self.questions:\n import time\n start = time.time() # A good proxy for setup time is to\n suite = loader.loadTestsFromTestCase(q)\n unittest.TextTestRunner(verbosity=verbosity).run(suite)\n total = time.time() - start\n q.time = total\n\n def _setup_answers(self):\n self.main() # Run all tests in class just to get that out of the way...\n report_cache = {}\n for q, _ in self.questions:\n if hasattr(q, \'_save_cache\'):\n q()._save_cache()\n q._cache[\'time\'] = q.time\n report_cache[q.__qualname__] = q._cache\n else:\n report_cache[q.__qualname__] = {\'no cache see _setup_answers in unitgrade2.py\':True}\n return report_cache\n\n def set_payload(self, payloads, strict=False):\n for q, _ in self.questions:\n q._cache = payloads[q.__qualname__]\n\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)\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]: # can perhaps be removed later.\n # item.title = payloads[q.name][item.name][\'title\']\n # except Exception as e: # Cannot set attribute error. The title is a function (and probably should not be).\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 self.n = int(np.round(self.t / self.dt))\n # self.pbar = tqdm.tqdm(total=self.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 self.time_started = time.time()\n\n def terminate(self):\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 return time.time() - self.time_started\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\n\nfrom unittest.suite import _isnotsuite\n\nclass MySuite(unittest.suite.TestSuite): # Not sure we need this one anymore.\n pass\n\ndef instance_call_stack(instance):\n s = "-".join(map(lambda x: x.__name__, instance.__class__.mro()))\n return s\n\ndef get_class_that_defined_method(meth):\n for cls in inspect.getmro(meth.im_class):\n if meth.__name__ in cls.__dict__:\n return cls\n return None\n\ndef caller_name(skip=2):\n """Get a name of a caller in the format module.class.method\n\n `skip` specifies how many levels of stack to skip while getting caller\n name. skip=1 means "who calls me", skip=2 "who calls my caller" etc.\n\n An empty string is returned if skipped levels exceed stack height\n """\n stack = inspect.stack()\n start = 0 + skip\n if len(stack) < start + 1:\n return \'\'\n parentframe = stack[start][0]\n\n name = []\n module = inspect.getmodule(parentframe)\n # `modname` can be None when frame is executed directly in console\n # TODO(techtonik): consider using __main__\n if module:\n name.append(module.__name__)\n # detect classname\n if \'self\' in parentframe.f_locals:\n # I don\'t know any way to detect call from the object method\n # XXX: there seems to be no way to detect static method call - it will\n # be just a function call\n name.append(parentframe.f_locals[\'self\'].__class__.__name__)\n codename = parentframe.f_code.co_name\n if codename != \'<module>\': # top level usually\n name.append( codename ) # function or a method\n\n ## Avoid circular refs and frame leaks\n # https://docs.python.org/2.7/library/inspect.html#the-interpreter-stack\n del parentframe, stack\n\n return ".".join(name)\n\ndef get_class_from_frame(fr):\n import inspect\n args, _, _, value_dict = inspect.getargvalues(fr)\n # we check the first parameter for the frame function is\n # named \'self\'\n if len(args) and args[0] == \'self\':\n # in that case, \'self\' will be referenced in value_dict\n instance = value_dict.get(\'self\', None)\n if instance:\n # return its class\n # isinstance(instance, Testing) # is the actual class instance.\n\n return getattr(instance, \'__class__\', None)\n # return None otherwise\n return None\n\nfrom typing import Any\nimport inspect, gc\n\ndef giveupthefunc():\n frame = inspect.currentframe()\n code = frame.f_code\n globs = frame.f_globals\n functype = type(lambda: 0)\n funcs = []\n for func in gc.get_referrers(code):\n if type(func) is functype:\n if getattr(func, "__code__", None) is code:\n if getattr(func, "__globals__", None) is globs:\n funcs.append(func)\n if len(funcs) > 1:\n return None\n return funcs[0] if funcs else None\n\n\nfrom collections import defaultdict\n\nclass UTextResult(unittest.TextTestResult):\n def __init__(self, stream, descriptions, verbosity):\n super().__init__(stream, descriptions, verbosity)\n self.successes = []\n\n def printErrors(self) -> None:\n # if self.dots or self.showAll:\n # self.stream.writeln()\n self.printErrorList(\'ERROR\', self.errors)\n self.printErrorList(\'FAIL\', self.failures)\n\n\n def addSuccess(self, test: unittest.case.TestCase) -> None:\n # super().addSuccess(test)\n self.successes.append(test)\n # super().addSuccess(test)\n # hidden = issubclass(item.__class__, Hidden)\n # # if not hidden:\n # # print(ss, end="")\n # # sys.stdout.flush()\n # start = time.time()\n #\n # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)\n # q_[j] = {\'w\': item.weight, \'possible\': possible, \'obtained\': current, \'hidden\': hidden, \'computed\': str(item._computed_answer), \'title\': item.title}\n # tsecs = np.round(time.time()-start, 2)\n show_progress_bar = True\n nL = 80\n if show_progress_bar:\n tsecs = np.round( self.cc.terminate(), 2)\n sys.stdout.flush()\n ss = self.item_title_print\n print(self.item_title_print + (\'.\' * max(0, nL - 4 - len(ss))), end="")\n #\n # if not hidden:\n current = 1\n possible = 1\n # tsecs = 2\n ss = "PASS" if current == possible else "*** FAILED"\n if tsecs >= 0.1:\n ss += " ("+ str(tsecs) + " seconds)"\n print(ss)\n\n\n def startTest(self, test):\n # super().startTest(test)\n self.testsRun += 1\n # print("Starting the test...")\n show_progress_bar = True\n n = 1\n j = 1\n item_title = self.getDescription(test)\n item_title = item_title.split("\\n")[0]\n self.item_title_print = "*** q%i.%i) %s" % (n + 1, j + 1, item_title)\n estimated_time = 10\n nL = 80\n #\n if show_progress_bar:\n self.cc = ActiveProgress(t=estimated_time, title=self.item_title_print)\n else:\n print(self.item_title_print + (\'.\' * max(0, nL - 4 - len(self.item_title_print))), end="")\n\n self._test = test\n\n def _setupStdout(self):\n if self._previousTestClass == None:\n total_estimated_time = 2\n if hasattr(self.__class__, \'q_title_print\'):\n q_title_print = self.__class__.q_title_print\n else:\n q_title_print = "<unnamed test. See unitgrade.py>"\n\n # q_title_print = "some printed title..."\n cc = ActiveProgress(t=total_estimated_time, title=q_title_print)\n self.cc = cc\n\n def _restoreStdout(self): # Used when setting up the test.\n if self._previousTestClass == None:\n q_time = self.cc.terminate()\n q_time = np.round(q_time, 2)\n sys.stdout.flush()\n print(self.cc.title, end="")\n # start = 10\n # q_time = np.round(time.time() - start, 2)\n nL = 80\n print(" " * max(0, nL - len(self.cc.title)) + (\n " (" + str(q_time) + " seconds)" if q_time >= 0.1 else "")) # if q.name in report.payloads else "")\n print("=" * nL)\n\nfrom unittest.runner import _WritelnDecorator\nfrom io import StringIO\n\nclass UTextTestRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n from io import StringIO\n stream = StringIO()\n super().__init__(*args, stream=stream, **kwargs)\n\n def _makeResult(self):\n stream = self.stream # not you!\n stream = sys.stdout\n stream = _WritelnDecorator(stream)\n return self.resultclass(stream, self.descriptions, self.verbosity)\n\ndef wrapper(foo):\n def magic(self):\n s = "-".join(map(lambda x: x.__name__, self.__class__.mro()))\n # print(s)\n foo(self)\n magic.__doc__ = foo.__doc__\n return magic\n\nfrom functools import update_wrapper, _make_key, RLock\nfrom collections import namedtuple\n_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])\n\ndef cache(foo, typed=False):\n """ Magic cache wrapper\n https://github.com/python/cpython/blob/main/Lib/functools.py\n """\n maxsize = None\n def wrapper(self, *args, **kwargs):\n key = self.cache_id() + ("cache", _make_key(args, kwargs, typed))\n if not self._cache_contains(key):\n value = foo(self, *args, **kwargs)\n self._cache_put(key, value)\n else:\n value = self._cache_get(key)\n return value\n return wrapper\n\n\nclass UTestCase(unittest.TestCase):\n _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache.\n _cache = None # Read-only cache.\n _cache2 = None # User-written cache\n\n @classmethod\n def reset(cls):\n cls._outcome = None\n cls._cache = None\n cls._cache2 = None\n\n def _get_outcome(self):\n if not (self.__class__, \'_outcome\') or self.__class__._outcome == None:\n self.__class__._outcome = {}\n return self.__class__._outcome\n\n def _callTestMethod(self, testMethod):\n t = time.time()\n res = testMethod()\n elapsed = time.time() - t\n # if res == None:\n # res = {}\n # res[\'time\'] = elapsed\n sd = self.shortDescription()\n self._cache_put( (self.cache_id(), \'title\'), self._testMethodName if sd == None else sd)\n # self._test_fun_output = res\n self._get_outcome()[self.cache_id()] = res\n self._cache_put( (self.cache_id(), "time"), elapsed)\n\n\n # This is my base test class. So what is new about it?\n def cache_id(self):\n c = self.__class__.__qualname__\n m = self._testMethodName\n return (c,m)\n\n def unique_cache_id(self):\n k0 = self.cache_id()\n key = ()\n for i in itertools.count():\n key = k0 + (i,)\n if not self._cache2_contains(key):\n break\n return key\n\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self._load_cache()\n self.cache_indexes = defaultdict(lambda: 0)\n\n def _ensure_cache_exists(self):\n if not hasattr(self.__class__, \'_cache\') or self.__class__._cache == None:\n self.__class__._cache = dict()\n if not hasattr(self.__class__, \'_cache2\') or self.__class__._cache2 == None:\n self.__class__._cache2 = dict()\n\n def _cache_get(self, key, default=None):\n self._ensure_cache_exists()\n return self.__class__._cache.get(key, default)\n\n def _cache_put(self, key, value):\n self._ensure_cache_exists()\n self.__class__._cache2[key] = value\n\n def _cache_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache\n\n def _cache2_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache2\n\n def assertEqualC(self, first: Any, msg: Any = ...) -> None:\n id = self.unique_cache_id()\n if not self._cache_contains(id):\n print("Warning, framework missing key", id)\n\n self.assertEqual(first, self._cache_get(id, first), msg)\n self._cache_put(id, first)\n\n def _cache_file(self):\n return os.path.dirname(inspect.getfile(self.__class__) ) + "/unitgrade/" + self.__class__.__name__ + ".pkl"\n\n def _save_cache(self):\n # get the class name (i.e. what to save to).\n cfile = self._cache_file()\n if not os.path.isdir(os.path.dirname(cfile)):\n os.makedirs(os.path.dirname(cfile))\n\n if hasattr(self.__class__, \'_cache2\'):\n with open(cfile, \'wb\') as f:\n pickle.dump(self.__class__._cache2, f)\n\n # But you can also set cache explicitly.\n def _load_cache(self):\n if self._cache != None: # Cache already loaded. We will not load it twice.\n return\n # raise Exception("Loaded cache which was already set. What is going on?!")\n cfile = self._cache_file()\n print("Loading cache from", cfile)\n if os.path.exists(cfile):\n with open(cfile, \'rb\') as f:\n data = pickle.load(f)\n self.__class__._cache = data\n else:\n print("Warning! data file not found", cfile)\n\ndef hide(func):\n return func\n\ndef makeRegisteringDecorator(foreignDecorator):\n """\n Returns a copy of foreignDecorator, which is identical in every\n way(*), except also appends a .decorator property to the callable it\n spits out.\n """\n def newDecorator(func):\n # Call to newDecorator(method)\n # Exactly like old decorator, but output keeps track of what decorated it\n R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done\n R.decorator = newDecorator # keep track of decorator\n # R.original = func # might as well keep track of everything!\n return R\n\n newDecorator.__name__ = foreignDecorator.__name__\n newDecorator.__doc__ = foreignDecorator.__doc__\n # (*)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\n return newDecorator\n\nhide = makeRegisteringDecorator(hide)\n\ndef methodsWithDecorator(cls, decorator):\n """\n Returns all methods in CLS with DECORATOR as the\n outermost decorator.\n\n DECORATOR must be a "registering decorator"; one\n can make any decorator "registering" via the\n makeRegisteringDecorator function.\n\n import inspect\n ls = list(methodsWithDecorator(GeneratorQuestion, deco))\n for f in ls:\n print(inspect.getsourcelines(f) ) # How to get all hidden questions.\n """\n for maybeDecorated in cls.__dict__.values():\n if hasattr(maybeDecorated, \'decorator\'):\n if maybeDecorated.decorator == decorator:\n print(maybeDecorated)\n yield maybeDecorated\n\n\n\nimport numpy as np\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport pyfiglet\nimport unittest\n\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\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\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 hasattr(report, "computed_answer_file") and 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\n # try: # For registering stats.\n # import unitgrade_private\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\n\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\nclass UnitgradeTextRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\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 hasattr(report, "version") and 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\n # Use the sequential test loader instead. See here:\n class SequentialTestLoader(unittest.TestLoader):\n def getTestCaseNames(self, testCaseClass):\n test_names = super().getTestCaseNames(testCaseClass)\n testcase_methods = list(testCaseClass.__dict__.keys())\n test_names.sort(key=testcase_methods.index)\n return test_names\n loader = SequentialTestLoader()\n # loader = unittest.TestLoader()\n # loader.suiteClass = MySuite\n\n for n, (q, w) in enumerate(report.questions):\n # q = q()\n q_hidden = False\n # q_hidden = issubclass(q.__class__, Hidden)\n if question is not None and n+1 != question:\n continue\n suite = loader.loadTestsFromTestCase(q)\n # print(suite)\n qtitle = q.__name__\n # qtitle = q.title if hasattr(q, "title") else q.id()\n # q.title = qtitle\n q_title_print = "Question %i: %s"%(n+1, qtitle)\n print(q_title_print, end="")\n q.possible = 0\n q.obtained = 0\n q_ = {} # Gather score in this class.\n # unittest.Te\n # q_with_outstanding_init = [item.question for item in q.items if not item.question.has_called_init_]\n UTextResult.q_title_print = q_title_print # Hacky\n res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite)\n # res = UTextTestRunner(verbosity=2, resultclass=unittest.TextTestResult).run(suite)\n z = 234\n # for j, item in enumerate(q.items):\n # if qitem is not None and question is not None and j+1 != qitem:\n # continue\n #\n # if q_with_outstanding_init is not None: # check for None bc. this must be called to set titles.\n # # if not item.question.has_called_init_:\n # start = time.time()\n #\n # cc = None\n # if show_progress_bar:\n # 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] )\n # cc = ActiveProgress(t=total_estimated_time, title=q_title_print)\n # from unitgrade import Capturing # DON\'T REMOVE THIS LINE\n # with eval(\'Capturing\')(unmute=unmute): # Clunky import syntax is required bc. of minify issue.\n # try:\n # for q2 in q_with_outstanding_init:\n # q2.init()\n # q2.has_called_init_ = True\n #\n # # item.question.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_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 # q_with_outstanding_init = None\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 #\n # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)\n # q_[j] = {\'w\': item.weight, \'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\n possible = res.testsRun\n obtained = possible - len(res.errors)\n\n\n # possible = int(ws @ possible)\n # obtained = int(ws @ obtained)\n # obtained = int(myround(int((w * obtained) / possible ))) if possible > 0 else 0\n\n obtained = w * int(obtained * 1.0 / possible )\n score[n] = {\'w\': w, \'possible\': w, \'obtained\': obtained, \'items\': q_, \'title\': qtitle}\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\n\n\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport inspect\nimport json\nimport os\nimport bz2\nimport pickle\nimport os\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 = str(__import__(m.__name__.split(\'.\')[0]).__path__)\n if m.__class__.__name__ == \'module\' and False:\n top_package = os.path.dirname(m.__file__)\n module_import = True\n else:\n top_package = __import__(m.__name__.split(\'.\')[0]).__path__._path[0]\n module_import = False\n\n # top_package = os.path.dirname(__import__(m.__name__.split(\'.\')[0]).__file__)\n # top_package = os.path.dirname(top_package)\n import zipfile\n # import strea\n # zipfile.ZipFile\n import io\n # file_like_object = io.BytesIO(my_zip_data)\n zip_buffer = io.BytesIO()\n with zipfile.ZipFile(zip_buffer, \'w\') as zip:\n # zip.write()\n for root, dirs, files in os.walk(top_package):\n for file in files:\n if file.endswith(".py"):\n fpath = os.path.join(root, file)\n v = os.path.relpath(os.path.join(root, file), os.path.dirname(top_package))\n zip.write(fpath, v)\n\n resources[\'zipfile\'] = zip_buffer.getvalue()\n resources[\'top_package\'] = top_package\n resources[\'module_import\'] = module_import\n return resources, 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 k, m in enumerate(report.pack_imports):\n nimp, top_package = gather_imports(m)\n report_relative_location = os.path.relpath(inspect.getfile(report.__class__), top_package)\n nimp[\'report_relative_location\'] = report_relative_location\n nimp[\'name\'] = m.__name__\n sources[k] = nimp\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 with open(token, \'wb\') as f:\n pickle.dump(results, f)\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.9.0"\n\n\nclass Week1(UTestCase):\n """ The first question for week 1. """\n def test_add(self):\n from cs103.homework1 import add\n self.assertEqualC(add(2,2))\n self.assertEqualC(add(-100, 5))\n\n @hide\n def test_add_hidden(self):\n # This is a hidden test. The @hide-decorator will allow unitgrade to remove the test.\n # See the output in the student directory for more information.\n from cs103.homework1 import add\n self.assertEqualC(add(2,2))\n\nimport cs103\nclass Report3(Report):\n title = "CS 101 Report 3"\n questions = [(Week1, 20)] # Include a single question for 10 credits.\n pack_imports = [cs103]' -report1_payload = '80049570000000000000007d948c055765656b31947d94288c055765656b31948c08746573745f616464944b0087944b04680368044b0187944aa1ffffff6803680486948c057469746c6594869468046803680486948c0474696d659486944700000000000000008c0474696d6594473f505b000000000075732e' +report1_payload = '80049570000000000000007d948c055765656b31947d94288c055765656b31948c08746573745f616464944b0087944b04680368044b0187944aa1ffffff6803680486948c057469746c6594869468046803680486948c0474696d659486944700000000000000008c0474696d6594473f6066800000000075732e' name="Report3" report = source_instantiate(name, report1_source, report1_payload) diff --git a/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/report3_grade.py b/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/report3_grade.py index efd3403..03baa4e 100644 --- a/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/report3_grade.py +++ b/examples/example_docker/instructor/unitgrade-docker/tmp/cs103/report3_grade.py @@ -431,7 +431,7 @@ def source_instantiate(name, report1_source, payload): report1_source = '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\nimport pickle\nimport itertools\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 weight = 1 # the weight of the question.\n\n def __init__(self, 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\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 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(f"Not equal within tolerance {tol=}; deviation was {np.max(diff)=}")\n print(f"Element-wise differences {diff.tolist()}")\n self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol=}, {np.max(diff)=}")\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 = "Untitled question"\n partially_scored = False\n t_init = 0 # Time spend on initialization (placeholder; set this externally).\n estimated_time = 0.42\n has_called_init_ = False\n _name = None\n _items = None\n\n @property\n def items(self):\n if self._items == 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 I in members:\n self._items.append( I(question=self))\n return self._items\n\n @items.setter\n def items(self, value):\n self._items = value\n\n @property\n def name(self):\n if self._name == None:\n self._name = self.__class__.__name__\n return self._name #\n\n @name.setter\n def name(self, val):\n self._name = val\n\n def init(self):\n # Can be used to set resources relevant for this question instance.\n pass\n\n def init_all_item_questions(self):\n for item in self.items:\n if not item.question.has_called_init_:\n item.question.init()\n item.question.has_called_init_ = True\n\n\nclass Report():\n title = "report title"\n version = None\n questions = []\n pack_imports = []\n individual_imports = []\n\n @classmethod\n def reset(cls):\n for (q,_) in cls.questions:\n if hasattr(q, \'reset\'):\n q.reset()\n\n def _file(self):\n return inspect.getfile(type(self))\n\n def __init__(self, strict=False, payload=None):\n working_directory = os.path.abspath(os.path.dirname(self._file()))\n\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\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 main(self, verbosity=1):\n # Run all tests using standard unittest (nothing fancy).\n import unittest\n loader = unittest.TestLoader()\n for q,_ in self.questions:\n import time\n start = time.time() # A good proxy for setup time is to\n suite = loader.loadTestsFromTestCase(q)\n unittest.TextTestRunner(verbosity=verbosity).run(suite)\n total = time.time() - start\n q.time = total\n\n def _setup_answers(self):\n self.main() # Run all tests in class just to get that out of the way...\n report_cache = {}\n for q, _ in self.questions:\n if hasattr(q, \'_save_cache\'):\n q()._save_cache()\n q._cache[\'time\'] = q.time\n report_cache[q.__qualname__] = q._cache\n else:\n report_cache[q.__qualname__] = {\'no cache see _setup_answers in unitgrade2.py\':True}\n return report_cache\n\n def set_payload(self, payloads, strict=False):\n for q, _ in self.questions:\n q._cache = payloads[q.__qualname__]\n\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)\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]: # can perhaps be removed later.\n # item.title = payloads[q.name][item.name][\'title\']\n # except Exception as e: # Cannot set attribute error. The title is a function (and probably should not be).\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 self.n = int(np.round(self.t / self.dt))\n # self.pbar = tqdm.tqdm(total=self.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 self.time_started = time.time()\n\n def terminate(self):\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 return time.time() - self.time_started\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\n\nfrom unittest.suite import _isnotsuite\n\nclass MySuite(unittest.suite.TestSuite): # Not sure we need this one anymore.\n pass\n\ndef instance_call_stack(instance):\n s = "-".join(map(lambda x: x.__name__, instance.__class__.mro()))\n return s\n\ndef get_class_that_defined_method(meth):\n for cls in inspect.getmro(meth.im_class):\n if meth.__name__ in cls.__dict__:\n return cls\n return None\n\ndef caller_name(skip=2):\n """Get a name of a caller in the format module.class.method\n\n `skip` specifies how many levels of stack to skip while getting caller\n name. skip=1 means "who calls me", skip=2 "who calls my caller" etc.\n\n An empty string is returned if skipped levels exceed stack height\n """\n stack = inspect.stack()\n start = 0 + skip\n if len(stack) < start + 1:\n return \'\'\n parentframe = stack[start][0]\n\n name = []\n module = inspect.getmodule(parentframe)\n # `modname` can be None when frame is executed directly in console\n # TODO(techtonik): consider using __main__\n if module:\n name.append(module.__name__)\n # detect classname\n if \'self\' in parentframe.f_locals:\n # I don\'t know any way to detect call from the object method\n # XXX: there seems to be no way to detect static method call - it will\n # be just a function call\n name.append(parentframe.f_locals[\'self\'].__class__.__name__)\n codename = parentframe.f_code.co_name\n if codename != \'<module>\': # top level usually\n name.append( codename ) # function or a method\n\n ## Avoid circular refs and frame leaks\n # https://docs.python.org/2.7/library/inspect.html#the-interpreter-stack\n del parentframe, stack\n\n return ".".join(name)\n\ndef get_class_from_frame(fr):\n import inspect\n args, _, _, value_dict = inspect.getargvalues(fr)\n # we check the first parameter for the frame function is\n # named \'self\'\n if len(args) and args[0] == \'self\':\n # in that case, \'self\' will be referenced in value_dict\n instance = value_dict.get(\'self\', None)\n if instance:\n # return its class\n # isinstance(instance, Testing) # is the actual class instance.\n\n return getattr(instance, \'__class__\', None)\n # return None otherwise\n return None\n\nfrom typing import Any\nimport inspect, gc\n\ndef giveupthefunc():\n frame = inspect.currentframe()\n code = frame.f_code\n globs = frame.f_globals\n functype = type(lambda: 0)\n funcs = []\n for func in gc.get_referrers(code):\n if type(func) is functype:\n if getattr(func, "__code__", None) is code:\n if getattr(func, "__globals__", None) is globs:\n funcs.append(func)\n if len(funcs) > 1:\n return None\n return funcs[0] if funcs else None\n\n\nfrom collections import defaultdict\n\nclass UTextResult(unittest.TextTestResult):\n def __init__(self, stream, descriptions, verbosity):\n super().__init__(stream, descriptions, verbosity)\n self.successes = []\n\n def printErrors(self) -> None:\n # if self.dots or self.showAll:\n # self.stream.writeln()\n self.printErrorList(\'ERROR\', self.errors)\n self.printErrorList(\'FAIL\', self.failures)\n\n\n def addSuccess(self, test: unittest.case.TestCase) -> None:\n # super().addSuccess(test)\n self.successes.append(test)\n # super().addSuccess(test)\n # hidden = issubclass(item.__class__, Hidden)\n # # if not hidden:\n # # print(ss, end="")\n # # sys.stdout.flush()\n # start = time.time()\n #\n # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)\n # q_[j] = {\'w\': item.weight, \'possible\': possible, \'obtained\': current, \'hidden\': hidden, \'computed\': str(item._computed_answer), \'title\': item.title}\n # tsecs = np.round(time.time()-start, 2)\n show_progress_bar = True\n nL = 80\n if show_progress_bar:\n tsecs = np.round( self.cc.terminate(), 2)\n sys.stdout.flush()\n ss = self.item_title_print\n print(self.item_title_print + (\'.\' * max(0, nL - 4 - len(ss))), end="")\n #\n # if not hidden:\n current = 1\n possible = 1\n # tsecs = 2\n ss = "PASS" if current == possible else "*** FAILED"\n if tsecs >= 0.1:\n ss += " ("+ str(tsecs) + " seconds)"\n print(ss)\n\n\n def startTest(self, test):\n # super().startTest(test)\n self.testsRun += 1\n # print("Starting the test...")\n show_progress_bar = True\n n = 1\n j = 1\n item_title = self.getDescription(test)\n item_title = item_title.split("\\n")[0]\n self.item_title_print = "*** q%i.%i) %s" % (n + 1, j + 1, item_title)\n estimated_time = 10\n nL = 80\n #\n if show_progress_bar:\n self.cc = ActiveProgress(t=estimated_time, title=self.item_title_print)\n else:\n print(self.item_title_print + (\'.\' * max(0, nL - 4 - len(self.item_title_print))), end="")\n\n self._test = test\n\n def _setupStdout(self):\n if self._previousTestClass == None:\n total_estimated_time = 2\n if hasattr(self.__class__, \'q_title_print\'):\n q_title_print = self.__class__.q_title_print\n else:\n q_title_print = "<unnamed test. See unitgrade.py>"\n\n # q_title_print = "some printed title..."\n cc = ActiveProgress(t=total_estimated_time, title=q_title_print)\n self.cc = cc\n\n def _restoreStdout(self): # Used when setting up the test.\n if self._previousTestClass == None:\n q_time = self.cc.terminate()\n q_time = np.round(q_time, 2)\n sys.stdout.flush()\n print(self.cc.title, end="")\n # start = 10\n # q_time = np.round(time.time() - start, 2)\n nL = 80\n print(" " * max(0, nL - len(self.cc.title)) + (\n " (" + str(q_time) + " seconds)" if q_time >= 0.1 else "")) # if q.name in report.payloads else "")\n print("=" * nL)\n\nfrom unittest.runner import _WritelnDecorator\nfrom io import StringIO\n\nclass UTextTestRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n from io import StringIO\n stream = StringIO()\n super().__init__(*args, stream=stream, **kwargs)\n\n def _makeResult(self):\n stream = self.stream # not you!\n stream = sys.stdout\n stream = _WritelnDecorator(stream)\n return self.resultclass(stream, self.descriptions, self.verbosity)\n\ndef wrapper(foo):\n def magic(self):\n s = "-".join(map(lambda x: x.__name__, self.__class__.mro()))\n # print(s)\n foo(self)\n magic.__doc__ = foo.__doc__\n return magic\n\nfrom functools import update_wrapper, _make_key, RLock\nfrom collections import namedtuple\n_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])\n\ndef cache(foo, typed=False):\n """ Magic cache wrapper\n https://github.com/python/cpython/blob/main/Lib/functools.py\n """\n maxsize = None\n def wrapper(self, *args, **kwargs):\n key = self.cache_id() + ("cache", _make_key(args, kwargs, typed))\n if not self._cache_contains(key):\n value = foo(self, *args, **kwargs)\n self._cache_put(key, value)\n else:\n value = self._cache_get(key)\n return value\n return wrapper\n\n\nclass UTestCase(unittest.TestCase):\n _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache.\n _cache = None # Read-only cache.\n _cache2 = None # User-written cache\n\n @classmethod\n def reset(cls):\n cls._outcome = None\n cls._cache = None\n cls._cache2 = None\n\n def _get_outcome(self):\n if not (self.__class__, \'_outcome\') or self.__class__._outcome == None:\n self.__class__._outcome = {}\n return self.__class__._outcome\n\n def _callTestMethod(self, testMethod):\n t = time.time()\n res = testMethod()\n elapsed = time.time() - t\n # if res == None:\n # res = {}\n # res[\'time\'] = elapsed\n sd = self.shortDescription()\n self._cache_put( (self.cache_id(), \'title\'), self._testMethodName if sd == None else sd)\n # self._test_fun_output = res\n self._get_outcome()[self.cache_id()] = res\n self._cache_put( (self.cache_id(), "time"), elapsed)\n\n\n # This is my base test class. So what is new about it?\n def cache_id(self):\n c = self.__class__.__qualname__\n m = self._testMethodName\n return (c,m)\n\n def unique_cache_id(self):\n k0 = self.cache_id()\n key = ()\n for i in itertools.count():\n key = k0 + (i,)\n if not self._cache2_contains(key):\n break\n return key\n\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self._load_cache()\n self.cache_indexes = defaultdict(lambda: 0)\n\n def _ensure_cache_exists(self):\n if not hasattr(self.__class__, \'_cache\') or self.__class__._cache == None:\n self.__class__._cache = dict()\n if not hasattr(self.__class__, \'_cache2\') or self.__class__._cache2 == None:\n self.__class__._cache2 = dict()\n\n def _cache_get(self, key, default=None):\n self._ensure_cache_exists()\n return self.__class__._cache.get(key, default)\n\n def _cache_put(self, key, value):\n self._ensure_cache_exists()\n self.__class__._cache2[key] = value\n\n def _cache_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache\n\n def _cache2_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache2\n\n def assertEqualC(self, first: Any, msg: Any = ...) -> None:\n id = self.unique_cache_id()\n if not self._cache_contains(id):\n print("Warning, framework missing key", id)\n\n self.assertEqual(first, self._cache_get(id, first), msg)\n self._cache_put(id, first)\n\n def _cache_file(self):\n return os.path.dirname(inspect.getfile(self.__class__) ) + "/unitgrade/" + self.__class__.__name__ + ".pkl"\n\n def _save_cache(self):\n # get the class name (i.e. what to save to).\n cfile = self._cache_file()\n if not os.path.isdir(os.path.dirname(cfile)):\n os.makedirs(os.path.dirname(cfile))\n\n if hasattr(self.__class__, \'_cache2\'):\n with open(cfile, \'wb\') as f:\n pickle.dump(self.__class__._cache2, f)\n\n # But you can also set cache explicitly.\n def _load_cache(self):\n if self._cache != None: # Cache already loaded. We will not load it twice.\n return\n # raise Exception("Loaded cache which was already set. What is going on?!")\n cfile = self._cache_file()\n print("Loading cache from", cfile)\n if os.path.exists(cfile):\n with open(cfile, \'rb\') as f:\n data = pickle.load(f)\n self.__class__._cache = data\n else:\n print("Warning! data file not found", cfile)\n\ndef hide(func):\n return func\n\ndef makeRegisteringDecorator(foreignDecorator):\n """\n Returns a copy of foreignDecorator, which is identical in every\n way(*), except also appends a .decorator property to the callable it\n spits out.\n """\n def newDecorator(func):\n # Call to newDecorator(method)\n # Exactly like old decorator, but output keeps track of what decorated it\n R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done\n R.decorator = newDecorator # keep track of decorator\n # R.original = func # might as well keep track of everything!\n return R\n\n newDecorator.__name__ = foreignDecorator.__name__\n newDecorator.__doc__ = foreignDecorator.__doc__\n # (*)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\n return newDecorator\n\nhide = makeRegisteringDecorator(hide)\n\ndef methodsWithDecorator(cls, decorator):\n """\n Returns all methods in CLS with DECORATOR as the\n outermost decorator.\n\n DECORATOR must be a "registering decorator"; one\n can make any decorator "registering" via the\n makeRegisteringDecorator function.\n\n import inspect\n ls = list(methodsWithDecorator(GeneratorQuestion, deco))\n for f in ls:\n print(inspect.getsourcelines(f) ) # How to get all hidden questions.\n """\n for maybeDecorated in cls.__dict__.values():\n if hasattr(maybeDecorated, \'decorator\'):\n if maybeDecorated.decorator == decorator:\n print(maybeDecorated)\n yield maybeDecorated\n\n\n\nimport numpy as np\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport pyfiglet\nimport unittest\n\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\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\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 hasattr(report, "computed_answer_file") and 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\n # try: # For registering stats.\n # import unitgrade_private\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\n\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\nclass UnitgradeTextRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\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 hasattr(report, "version") and 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\n # Use the sequential test loader instead. See here:\n class SequentialTestLoader(unittest.TestLoader):\n def getTestCaseNames(self, testCaseClass):\n test_names = super().getTestCaseNames(testCaseClass)\n testcase_methods = list(testCaseClass.__dict__.keys())\n test_names.sort(key=testcase_methods.index)\n return test_names\n loader = SequentialTestLoader()\n # loader = unittest.TestLoader()\n # loader.suiteClass = MySuite\n\n for n, (q, w) in enumerate(report.questions):\n # q = q()\n q_hidden = False\n # q_hidden = issubclass(q.__class__, Hidden)\n if question is not None and n+1 != question:\n continue\n suite = loader.loadTestsFromTestCase(q)\n # print(suite)\n qtitle = q.__name__\n # qtitle = q.title if hasattr(q, "title") else q.id()\n # q.title = qtitle\n q_title_print = "Question %i: %s"%(n+1, qtitle)\n print(q_title_print, end="")\n q.possible = 0\n q.obtained = 0\n q_ = {} # Gather score in this class.\n # unittest.Te\n # q_with_outstanding_init = [item.question for item in q.items if not item.question.has_called_init_]\n UTextResult.q_title_print = q_title_print # Hacky\n res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite)\n # res = UTextTestRunner(verbosity=2, resultclass=unittest.TextTestResult).run(suite)\n z = 234\n # for j, item in enumerate(q.items):\n # if qitem is not None and question is not None and j+1 != qitem:\n # continue\n #\n # if q_with_outstanding_init is not None: # check for None bc. this must be called to set titles.\n # # if not item.question.has_called_init_:\n # start = time.time()\n #\n # cc = None\n # if show_progress_bar:\n # 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] )\n # cc = ActiveProgress(t=total_estimated_time, title=q_title_print)\n # from unitgrade import Capturing # DON\'T REMOVE THIS LINE\n # with eval(\'Capturing\')(unmute=unmute): # Clunky import syntax is required bc. of minify issue.\n # try:\n # for q2 in q_with_outstanding_init:\n # q2.init()\n # q2.has_called_init_ = True\n #\n # # item.question.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_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 # q_with_outstanding_init = None\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 #\n # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)\n # q_[j] = {\'w\': item.weight, \'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\n possible = res.testsRun\n obtained = possible - len(res.errors)\n\n\n # possible = int(ws @ possible)\n # obtained = int(ws @ obtained)\n # obtained = int(myround(int((w * obtained) / possible ))) if possible > 0 else 0\n\n obtained = w * int(obtained * 1.0 / possible )\n score[n] = {\'w\': w, \'possible\': w, \'obtained\': obtained, \'items\': q_, \'title\': qtitle}\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\n\n\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport inspect\nimport json\nimport os\nimport bz2\nimport pickle\nimport os\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 = str(__import__(m.__name__.split(\'.\')[0]).__path__)\n if m.__class__.__name__ == \'module\' and False:\n top_package = os.path.dirname(m.__file__)\n module_import = True\n else:\n top_package = __import__(m.__name__.split(\'.\')[0]).__path__._path[0]\n module_import = False\n\n # top_package = os.path.dirname(__import__(m.__name__.split(\'.\')[0]).__file__)\n # top_package = os.path.dirname(top_package)\n import zipfile\n # import strea\n # zipfile.ZipFile\n import io\n # file_like_object = io.BytesIO(my_zip_data)\n zip_buffer = io.BytesIO()\n with zipfile.ZipFile(zip_buffer, \'w\') as zip:\n # zip.write()\n for root, dirs, files in os.walk(top_package):\n for file in files:\n if file.endswith(".py"):\n fpath = os.path.join(root, file)\n v = os.path.relpath(os.path.join(root, file), os.path.dirname(top_package))\n zip.write(fpath, v)\n\n resources[\'zipfile\'] = zip_buffer.getvalue()\n resources[\'top_package\'] = top_package\n resources[\'module_import\'] = module_import\n return resources, 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 k, m in enumerate(report.pack_imports):\n nimp, top_package = gather_imports(m)\n report_relative_location = os.path.relpath(inspect.getfile(report.__class__), top_package)\n nimp[\'report_relative_location\'] = report_relative_location\n nimp[\'name\'] = m.__name__\n sources[k] = nimp\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 with open(token, \'wb\') as f:\n pickle.dump(results, f)\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.9.0"\n\n\nclass Week1(UTestCase):\n """ The first question for week 1. """\n def test_add(self):\n from cs103.homework1 import add\n self.assertEqualC(add(2,2))\n self.assertEqualC(add(-100, 5))\n\n\nimport cs103\nclass Report3(Report):\n title = "CS 101 Report 3"\n questions = [(Week1, 20)] # Include a single question for 10 credits.\n pack_imports = [cs103]' -report1_payload = '800495a9000000000000007d948c055765656b31947d94288c055765656b31948c08746573745f616464944b0087944b04680368044b0187944aa1ffffff6803680486948c057469746c6594869468046803680486948c0474696d65948694473f505b000000000068038c0f746573745f6164645f68696464656e944b0087944b046803680d869468088694680d6803680d8694680b86944700000000000000008c0474696d6594473f805fc00000000075732e' +report1_payload = '800495a9000000000000007d948c055765656b31947d94288c055765656b31948c08746573745f616464944b0087944b04680368044b0187944aa1ffffff6803680486948c057469746c6594869468046803680486948c0474696d65948694473f5061000000000068038c0f746573745f6164645f68696464656e944b0087944b046803680d869468088694680d6803680d8694680b86944700000000000000008c0474696d6594473f7ca5000000000075732e' name="Report3" report = source_instantiate(name, report1_source, report1_payload) diff --git a/examples/example_docker/students/cs103/Report3_handin_0_of_20.token b/examples/example_docker/students/cs103/Report3_handin_0_of_20.token index 347cfbde41438ca1ba78ee4d4e7953e7b8ce3108..89d9eaa50e45e079355529ae91c29faf094d2851 100644 GIT binary patch delta 87 zcmeBJ!qTyXWx`~}g&U`&GBYmR{EPV&6UV|Bso;qX#uu93ux)?C#%RRBWM(*BfuFID m)jT=T)L{B%e#SG53#T^-FxoRNoPI!n(H5>{JF6fgGa~?)vK`?7 delta 87 zcmeBJ!qTyXWx`}erj1ilnHiZj|6+c{#K9CJ6`Z?y@tNi~Y}?<kF&c3&nI=tF;AiY( mwJ<PEOP;=&pYaSM)AR-bMter4=?4TDZQ)wBvkEdYGXek<%o|ex diff --git a/examples/example_docker/students/cs103/__pycache__/homework1.cpython-38.pyc b/examples/example_docker/students/cs103/__pycache__/homework1.cpython-38.pyc index a01edf9d5079a89fbce46b7ed3c9b3bedffaec9f..77bcba4d2fb526f7d14ec3f8c1d6ac95ddeae116 100644 GIT binary patch delta 19 acmaFB{(zk;l$V!_fq{Wxf5JwtYs><LlFG delta 19 acmaFB{(zk;l$V!_fq{WxYWzm7Ys>&KS_Ikv diff --git a/examples/example_docker/students/cs103/__pycache__/report3.cpython-38.pyc b/examples/example_docker/students/cs103/__pycache__/report3.cpython-38.pyc index 0edd5337296831e5e9223e6b60b503ad093cf02e..e5e928b3cf8c95f9ee7e0283457b713591a406e4 100644 GIT binary patch delta 19 acmZ3$v4Ddsl$V!_fq{WxU&2N%F%|$P_yflP delta 19 acmZ3$v4Ddsl$V!_fq{WxO8iDHF%|$O_XC^& diff --git a/examples/example_docker/students/cs103/__pycache__/report3_grade.cpython-38.pyc b/examples/example_docker/students/cs103/__pycache__/report3_grade.cpython-38.pyc index e31d630d54cae23263b1a11b45c49a35268949dd..e1fe41d0d88015abab6229a54f2d8f5e6a8eb04c 100644 GIT binary patch delta 39 vcmX?ig!$YNX0A|PUM>a(28R6!8@XQGXEHOK{N;Wht9f#wsln#<2gex!7NQRG delta 39 vcmX?ig!$YNX0A|PUM>a(28OBe8@XQGXEIHi{N;WhtA&ASTJq-h2gex!6#@?a diff --git a/examples/example_docker/students/cs103/report3_grade.py b/examples/example_docker/students/cs103/report3_grade.py index efd3403..03baa4e 100644 --- a/examples/example_docker/students/cs103/report3_grade.py +++ b/examples/example_docker/students/cs103/report3_grade.py @@ -431,7 +431,7 @@ def source_instantiate(name, report1_source, payload): report1_source = '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\nimport pickle\nimport itertools\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 weight = 1 # the weight of the question.\n\n def __init__(self, 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\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 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(f"Not equal within tolerance {tol=}; deviation was {np.max(diff)=}")\n print(f"Element-wise differences {diff.tolist()}")\n self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol=}, {np.max(diff)=}")\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 = "Untitled question"\n partially_scored = False\n t_init = 0 # Time spend on initialization (placeholder; set this externally).\n estimated_time = 0.42\n has_called_init_ = False\n _name = None\n _items = None\n\n @property\n def items(self):\n if self._items == 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 I in members:\n self._items.append( I(question=self))\n return self._items\n\n @items.setter\n def items(self, value):\n self._items = value\n\n @property\n def name(self):\n if self._name == None:\n self._name = self.__class__.__name__\n return self._name #\n\n @name.setter\n def name(self, val):\n self._name = val\n\n def init(self):\n # Can be used to set resources relevant for this question instance.\n pass\n\n def init_all_item_questions(self):\n for item in self.items:\n if not item.question.has_called_init_:\n item.question.init()\n item.question.has_called_init_ = True\n\n\nclass Report():\n title = "report title"\n version = None\n questions = []\n pack_imports = []\n individual_imports = []\n\n @classmethod\n def reset(cls):\n for (q,_) in cls.questions:\n if hasattr(q, \'reset\'):\n q.reset()\n\n def _file(self):\n return inspect.getfile(type(self))\n\n def __init__(self, strict=False, payload=None):\n working_directory = os.path.abspath(os.path.dirname(self._file()))\n\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\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 main(self, verbosity=1):\n # Run all tests using standard unittest (nothing fancy).\n import unittest\n loader = unittest.TestLoader()\n for q,_ in self.questions:\n import time\n start = time.time() # A good proxy for setup time is to\n suite = loader.loadTestsFromTestCase(q)\n unittest.TextTestRunner(verbosity=verbosity).run(suite)\n total = time.time() - start\n q.time = total\n\n def _setup_answers(self):\n self.main() # Run all tests in class just to get that out of the way...\n report_cache = {}\n for q, _ in self.questions:\n if hasattr(q, \'_save_cache\'):\n q()._save_cache()\n q._cache[\'time\'] = q.time\n report_cache[q.__qualname__] = q._cache\n else:\n report_cache[q.__qualname__] = {\'no cache see _setup_answers in unitgrade2.py\':True}\n return report_cache\n\n def set_payload(self, payloads, strict=False):\n for q, _ in self.questions:\n q._cache = payloads[q.__qualname__]\n\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)\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]: # can perhaps be removed later.\n # item.title = payloads[q.name][item.name][\'title\']\n # except Exception as e: # Cannot set attribute error. The title is a function (and probably should not be).\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 self.n = int(np.round(self.t / self.dt))\n # self.pbar = tqdm.tqdm(total=self.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 self.time_started = time.time()\n\n def terminate(self):\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 return time.time() - self.time_started\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\n\nfrom unittest.suite import _isnotsuite\n\nclass MySuite(unittest.suite.TestSuite): # Not sure we need this one anymore.\n pass\n\ndef instance_call_stack(instance):\n s = "-".join(map(lambda x: x.__name__, instance.__class__.mro()))\n return s\n\ndef get_class_that_defined_method(meth):\n for cls in inspect.getmro(meth.im_class):\n if meth.__name__ in cls.__dict__:\n return cls\n return None\n\ndef caller_name(skip=2):\n """Get a name of a caller in the format module.class.method\n\n `skip` specifies how many levels of stack to skip while getting caller\n name. skip=1 means "who calls me", skip=2 "who calls my caller" etc.\n\n An empty string is returned if skipped levels exceed stack height\n """\n stack = inspect.stack()\n start = 0 + skip\n if len(stack) < start + 1:\n return \'\'\n parentframe = stack[start][0]\n\n name = []\n module = inspect.getmodule(parentframe)\n # `modname` can be None when frame is executed directly in console\n # TODO(techtonik): consider using __main__\n if module:\n name.append(module.__name__)\n # detect classname\n if \'self\' in parentframe.f_locals:\n # I don\'t know any way to detect call from the object method\n # XXX: there seems to be no way to detect static method call - it will\n # be just a function call\n name.append(parentframe.f_locals[\'self\'].__class__.__name__)\n codename = parentframe.f_code.co_name\n if codename != \'<module>\': # top level usually\n name.append( codename ) # function or a method\n\n ## Avoid circular refs and frame leaks\n # https://docs.python.org/2.7/library/inspect.html#the-interpreter-stack\n del parentframe, stack\n\n return ".".join(name)\n\ndef get_class_from_frame(fr):\n import inspect\n args, _, _, value_dict = inspect.getargvalues(fr)\n # we check the first parameter for the frame function is\n # named \'self\'\n if len(args) and args[0] == \'self\':\n # in that case, \'self\' will be referenced in value_dict\n instance = value_dict.get(\'self\', None)\n if instance:\n # return its class\n # isinstance(instance, Testing) # is the actual class instance.\n\n return getattr(instance, \'__class__\', None)\n # return None otherwise\n return None\n\nfrom typing import Any\nimport inspect, gc\n\ndef giveupthefunc():\n frame = inspect.currentframe()\n code = frame.f_code\n globs = frame.f_globals\n functype = type(lambda: 0)\n funcs = []\n for func in gc.get_referrers(code):\n if type(func) is functype:\n if getattr(func, "__code__", None) is code:\n if getattr(func, "__globals__", None) is globs:\n funcs.append(func)\n if len(funcs) > 1:\n return None\n return funcs[0] if funcs else None\n\n\nfrom collections import defaultdict\n\nclass UTextResult(unittest.TextTestResult):\n def __init__(self, stream, descriptions, verbosity):\n super().__init__(stream, descriptions, verbosity)\n self.successes = []\n\n def printErrors(self) -> None:\n # if self.dots or self.showAll:\n # self.stream.writeln()\n self.printErrorList(\'ERROR\', self.errors)\n self.printErrorList(\'FAIL\', self.failures)\n\n\n def addSuccess(self, test: unittest.case.TestCase) -> None:\n # super().addSuccess(test)\n self.successes.append(test)\n # super().addSuccess(test)\n # hidden = issubclass(item.__class__, Hidden)\n # # if not hidden:\n # # print(ss, end="")\n # # sys.stdout.flush()\n # start = time.time()\n #\n # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)\n # q_[j] = {\'w\': item.weight, \'possible\': possible, \'obtained\': current, \'hidden\': hidden, \'computed\': str(item._computed_answer), \'title\': item.title}\n # tsecs = np.round(time.time()-start, 2)\n show_progress_bar = True\n nL = 80\n if show_progress_bar:\n tsecs = np.round( self.cc.terminate(), 2)\n sys.stdout.flush()\n ss = self.item_title_print\n print(self.item_title_print + (\'.\' * max(0, nL - 4 - len(ss))), end="")\n #\n # if not hidden:\n current = 1\n possible = 1\n # tsecs = 2\n ss = "PASS" if current == possible else "*** FAILED"\n if tsecs >= 0.1:\n ss += " ("+ str(tsecs) + " seconds)"\n print(ss)\n\n\n def startTest(self, test):\n # super().startTest(test)\n self.testsRun += 1\n # print("Starting the test...")\n show_progress_bar = True\n n = 1\n j = 1\n item_title = self.getDescription(test)\n item_title = item_title.split("\\n")[0]\n self.item_title_print = "*** q%i.%i) %s" % (n + 1, j + 1, item_title)\n estimated_time = 10\n nL = 80\n #\n if show_progress_bar:\n self.cc = ActiveProgress(t=estimated_time, title=self.item_title_print)\n else:\n print(self.item_title_print + (\'.\' * max(0, nL - 4 - len(self.item_title_print))), end="")\n\n self._test = test\n\n def _setupStdout(self):\n if self._previousTestClass == None:\n total_estimated_time = 2\n if hasattr(self.__class__, \'q_title_print\'):\n q_title_print = self.__class__.q_title_print\n else:\n q_title_print = "<unnamed test. See unitgrade.py>"\n\n # q_title_print = "some printed title..."\n cc = ActiveProgress(t=total_estimated_time, title=q_title_print)\n self.cc = cc\n\n def _restoreStdout(self): # Used when setting up the test.\n if self._previousTestClass == None:\n q_time = self.cc.terminate()\n q_time = np.round(q_time, 2)\n sys.stdout.flush()\n print(self.cc.title, end="")\n # start = 10\n # q_time = np.round(time.time() - start, 2)\n nL = 80\n print(" " * max(0, nL - len(self.cc.title)) + (\n " (" + str(q_time) + " seconds)" if q_time >= 0.1 else "")) # if q.name in report.payloads else "")\n print("=" * nL)\n\nfrom unittest.runner import _WritelnDecorator\nfrom io import StringIO\n\nclass UTextTestRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n from io import StringIO\n stream = StringIO()\n super().__init__(*args, stream=stream, **kwargs)\n\n def _makeResult(self):\n stream = self.stream # not you!\n stream = sys.stdout\n stream = _WritelnDecorator(stream)\n return self.resultclass(stream, self.descriptions, self.verbosity)\n\ndef wrapper(foo):\n def magic(self):\n s = "-".join(map(lambda x: x.__name__, self.__class__.mro()))\n # print(s)\n foo(self)\n magic.__doc__ = foo.__doc__\n return magic\n\nfrom functools import update_wrapper, _make_key, RLock\nfrom collections import namedtuple\n_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])\n\ndef cache(foo, typed=False):\n """ Magic cache wrapper\n https://github.com/python/cpython/blob/main/Lib/functools.py\n """\n maxsize = None\n def wrapper(self, *args, **kwargs):\n key = self.cache_id() + ("cache", _make_key(args, kwargs, typed))\n if not self._cache_contains(key):\n value = foo(self, *args, **kwargs)\n self._cache_put(key, value)\n else:\n value = self._cache_get(key)\n return value\n return wrapper\n\n\nclass UTestCase(unittest.TestCase):\n _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache.\n _cache = None # Read-only cache.\n _cache2 = None # User-written cache\n\n @classmethod\n def reset(cls):\n cls._outcome = None\n cls._cache = None\n cls._cache2 = None\n\n def _get_outcome(self):\n if not (self.__class__, \'_outcome\') or self.__class__._outcome == None:\n self.__class__._outcome = {}\n return self.__class__._outcome\n\n def _callTestMethod(self, testMethod):\n t = time.time()\n res = testMethod()\n elapsed = time.time() - t\n # if res == None:\n # res = {}\n # res[\'time\'] = elapsed\n sd = self.shortDescription()\n self._cache_put( (self.cache_id(), \'title\'), self._testMethodName if sd == None else sd)\n # self._test_fun_output = res\n self._get_outcome()[self.cache_id()] = res\n self._cache_put( (self.cache_id(), "time"), elapsed)\n\n\n # This is my base test class. So what is new about it?\n def cache_id(self):\n c = self.__class__.__qualname__\n m = self._testMethodName\n return (c,m)\n\n def unique_cache_id(self):\n k0 = self.cache_id()\n key = ()\n for i in itertools.count():\n key = k0 + (i,)\n if not self._cache2_contains(key):\n break\n return key\n\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self._load_cache()\n self.cache_indexes = defaultdict(lambda: 0)\n\n def _ensure_cache_exists(self):\n if not hasattr(self.__class__, \'_cache\') or self.__class__._cache == None:\n self.__class__._cache = dict()\n if not hasattr(self.__class__, \'_cache2\') or self.__class__._cache2 == None:\n self.__class__._cache2 = dict()\n\n def _cache_get(self, key, default=None):\n self._ensure_cache_exists()\n return self.__class__._cache.get(key, default)\n\n def _cache_put(self, key, value):\n self._ensure_cache_exists()\n self.__class__._cache2[key] = value\n\n def _cache_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache\n\n def _cache2_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache2\n\n def assertEqualC(self, first: Any, msg: Any = ...) -> None:\n id = self.unique_cache_id()\n if not self._cache_contains(id):\n print("Warning, framework missing key", id)\n\n self.assertEqual(first, self._cache_get(id, first), msg)\n self._cache_put(id, first)\n\n def _cache_file(self):\n return os.path.dirname(inspect.getfile(self.__class__) ) + "/unitgrade/" + self.__class__.__name__ + ".pkl"\n\n def _save_cache(self):\n # get the class name (i.e. what to save to).\n cfile = self._cache_file()\n if not os.path.isdir(os.path.dirname(cfile)):\n os.makedirs(os.path.dirname(cfile))\n\n if hasattr(self.__class__, \'_cache2\'):\n with open(cfile, \'wb\') as f:\n pickle.dump(self.__class__._cache2, f)\n\n # But you can also set cache explicitly.\n def _load_cache(self):\n if self._cache != None: # Cache already loaded. We will not load it twice.\n return\n # raise Exception("Loaded cache which was already set. What is going on?!")\n cfile = self._cache_file()\n print("Loading cache from", cfile)\n if os.path.exists(cfile):\n with open(cfile, \'rb\') as f:\n data = pickle.load(f)\n self.__class__._cache = data\n else:\n print("Warning! data file not found", cfile)\n\ndef hide(func):\n return func\n\ndef makeRegisteringDecorator(foreignDecorator):\n """\n Returns a copy of foreignDecorator, which is identical in every\n way(*), except also appends a .decorator property to the callable it\n spits out.\n """\n def newDecorator(func):\n # Call to newDecorator(method)\n # Exactly like old decorator, but output keeps track of what decorated it\n R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done\n R.decorator = newDecorator # keep track of decorator\n # R.original = func # might as well keep track of everything!\n return R\n\n newDecorator.__name__ = foreignDecorator.__name__\n newDecorator.__doc__ = foreignDecorator.__doc__\n # (*)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\n return newDecorator\n\nhide = makeRegisteringDecorator(hide)\n\ndef methodsWithDecorator(cls, decorator):\n """\n Returns all methods in CLS with DECORATOR as the\n outermost decorator.\n\n DECORATOR must be a "registering decorator"; one\n can make any decorator "registering" via the\n makeRegisteringDecorator function.\n\n import inspect\n ls = list(methodsWithDecorator(GeneratorQuestion, deco))\n for f in ls:\n print(inspect.getsourcelines(f) ) # How to get all hidden questions.\n """\n for maybeDecorated in cls.__dict__.values():\n if hasattr(maybeDecorated, \'decorator\'):\n if maybeDecorated.decorator == decorator:\n print(maybeDecorated)\n yield maybeDecorated\n\n\n\nimport numpy as np\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport pyfiglet\nimport unittest\n\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\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\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 hasattr(report, "computed_answer_file") and 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\n # try: # For registering stats.\n # import unitgrade_private\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\n\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\nclass UnitgradeTextRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\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 hasattr(report, "version") and 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\n # Use the sequential test loader instead. See here:\n class SequentialTestLoader(unittest.TestLoader):\n def getTestCaseNames(self, testCaseClass):\n test_names = super().getTestCaseNames(testCaseClass)\n testcase_methods = list(testCaseClass.__dict__.keys())\n test_names.sort(key=testcase_methods.index)\n return test_names\n loader = SequentialTestLoader()\n # loader = unittest.TestLoader()\n # loader.suiteClass = MySuite\n\n for n, (q, w) in enumerate(report.questions):\n # q = q()\n q_hidden = False\n # q_hidden = issubclass(q.__class__, Hidden)\n if question is not None and n+1 != question:\n continue\n suite = loader.loadTestsFromTestCase(q)\n # print(suite)\n qtitle = q.__name__\n # qtitle = q.title if hasattr(q, "title") else q.id()\n # q.title = qtitle\n q_title_print = "Question %i: %s"%(n+1, qtitle)\n print(q_title_print, end="")\n q.possible = 0\n q.obtained = 0\n q_ = {} # Gather score in this class.\n # unittest.Te\n # q_with_outstanding_init = [item.question for item in q.items if not item.question.has_called_init_]\n UTextResult.q_title_print = q_title_print # Hacky\n res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite)\n # res = UTextTestRunner(verbosity=2, resultclass=unittest.TextTestResult).run(suite)\n z = 234\n # for j, item in enumerate(q.items):\n # if qitem is not None and question is not None and j+1 != qitem:\n # continue\n #\n # if q_with_outstanding_init is not None: # check for None bc. this must be called to set titles.\n # # if not item.question.has_called_init_:\n # start = time.time()\n #\n # cc = None\n # if show_progress_bar:\n # 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] )\n # cc = ActiveProgress(t=total_estimated_time, title=q_title_print)\n # from unitgrade import Capturing # DON\'T REMOVE THIS LINE\n # with eval(\'Capturing\')(unmute=unmute): # Clunky import syntax is required bc. of minify issue.\n # try:\n # for q2 in q_with_outstanding_init:\n # q2.init()\n # q2.has_called_init_ = True\n #\n # # item.question.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_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 # q_with_outstanding_init = None\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 #\n # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)\n # q_[j] = {\'w\': item.weight, \'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\n possible = res.testsRun\n obtained = possible - len(res.errors)\n\n\n # possible = int(ws @ possible)\n # obtained = int(ws @ obtained)\n # obtained = int(myround(int((w * obtained) / possible ))) if possible > 0 else 0\n\n obtained = w * int(obtained * 1.0 / possible )\n score[n] = {\'w\': w, \'possible\': w, \'obtained\': obtained, \'items\': q_, \'title\': qtitle}\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\n\n\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport inspect\nimport json\nimport os\nimport bz2\nimport pickle\nimport os\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 = str(__import__(m.__name__.split(\'.\')[0]).__path__)\n if m.__class__.__name__ == \'module\' and False:\n top_package = os.path.dirname(m.__file__)\n module_import = True\n else:\n top_package = __import__(m.__name__.split(\'.\')[0]).__path__._path[0]\n module_import = False\n\n # top_package = os.path.dirname(__import__(m.__name__.split(\'.\')[0]).__file__)\n # top_package = os.path.dirname(top_package)\n import zipfile\n # import strea\n # zipfile.ZipFile\n import io\n # file_like_object = io.BytesIO(my_zip_data)\n zip_buffer = io.BytesIO()\n with zipfile.ZipFile(zip_buffer, \'w\') as zip:\n # zip.write()\n for root, dirs, files in os.walk(top_package):\n for file in files:\n if file.endswith(".py"):\n fpath = os.path.join(root, file)\n v = os.path.relpath(os.path.join(root, file), os.path.dirname(top_package))\n zip.write(fpath, v)\n\n resources[\'zipfile\'] = zip_buffer.getvalue()\n resources[\'top_package\'] = top_package\n resources[\'module_import\'] = module_import\n return resources, 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 k, m in enumerate(report.pack_imports):\n nimp, top_package = gather_imports(m)\n report_relative_location = os.path.relpath(inspect.getfile(report.__class__), top_package)\n nimp[\'report_relative_location\'] = report_relative_location\n nimp[\'name\'] = m.__name__\n sources[k] = nimp\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 with open(token, \'wb\') as f:\n pickle.dump(results, f)\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.9.0"\n\n\nclass Week1(UTestCase):\n """ The first question for week 1. """\n def test_add(self):\n from cs103.homework1 import add\n self.assertEqualC(add(2,2))\n self.assertEqualC(add(-100, 5))\n\n\nimport cs103\nclass Report3(Report):\n title = "CS 101 Report 3"\n questions = [(Week1, 20)] # Include a single question for 10 credits.\n pack_imports = [cs103]' -report1_payload = '800495a9000000000000007d948c055765656b31947d94288c055765656b31948c08746573745f616464944b0087944b04680368044b0187944aa1ffffff6803680486948c057469746c6594869468046803680486948c0474696d65948694473f505b000000000068038c0f746573745f6164645f68696464656e944b0087944b046803680d869468088694680d6803680d8694680b86944700000000000000008c0474696d6594473f805fc00000000075732e' +report1_payload = '800495a9000000000000007d948c055765656b31947d94288c055765656b31948c08746573745f616464944b0087944b04680368044b0187944aa1ffffff6803680486948c057469746c6594869468046803680486948c0474696d65948694473f5061000000000068038c0f746573745f6164645f68696464656e944b0087944b046803680d869468088694680d6803680d8694680b86944700000000000000008c0474696d6594473f7ca5000000000075732e' name="Report3" report = source_instantiate(name, report1_source, report1_payload) diff --git a/examples/example_framework/instructor/cs102/deploy.py b/examples/example_framework/instructor/cs102/deploy.py index 4cceb5f..c585908 100644 --- a/examples/example_framework/instructor/cs102/deploy.py +++ b/examples/example_framework/instructor/cs102/deploy.py @@ -6,4 +6,4 @@ if __name__ == "__main__": gather_upload_to_campusnet(Report2()) setup_grade_file_report(Report2, minify=False, obfuscate=False, execute=False) - snip_dir(source_dir="../cs102", dest_dir="../../students/cs102", clean_destination_dir=True, exclude=['*.token', 'deploy.py']) + snip_dir(source_dir="../cs102", dest_dir="../../students/cs102", clean_destination_dir=True, exclude=['__pycache__', '*.token', 'deploy.py']) diff --git a/examples/example_simplest/instructor/cs101/deploy.py b/examples/example_simplest/instructor/cs101/deploy.py index 8906532..3e9682d 100644 --- a/examples/example_simplest/instructor/cs101/deploy.py +++ b/examples/example_simplest/instructor/cs101/deploy.py @@ -2,15 +2,12 @@ from report1 import Report1 from unitgrade_private2.hidden_create_files import setup_grade_file_report from snipper import snip_dir import shutil -# from unitgrade_private2.hidden_gather_upload import gather_upload_to_campusnet if __name__ == "__main__": - # gather_upload_to_campusnet(Report1()) setup_grade_file_report(Report1, minify=False, obfuscate=False, execute=False) # Deploy the files using snipper: https://gitlab.compute.dtu.dk/tuhe/snipper - - snip_dir.snip_dir(source_dir="../cs101", dest_dir="../../students/cs101", clean_destination_dir=True, exclude=['*.token', 'deploy.py']) + snip_dir.snip_dir(source_dir="../cs101", dest_dir="../../students/cs101", clean_destination_dir=True, exclude=['__pycache__', '*.token', 'deploy.py']) # For my own sake, copy the homework to the other examples. for f in ['../../../example_framework/instructor/cs102/homework1.py', '../../../example_docker/instructor/cs103/homework1.py']: diff --git a/unitgrade_private2/__pycache__/docker_helpers.cpython-38.pyc b/unitgrade_private2/__pycache__/docker_helpers.cpython-38.pyc index 2bf0b8a3caada163a94b085cfc3a27fc88accc55..61e1a3ce0d3a883def761a67fc6197ab8888857b 100644 GIT binary patch delta 564 zcmZ3;f0&;)l$V!_fq{WxM}m6d(TThh8GR<s_b_G1VqCyf!?=*KD5`{G0VjyX$WX$y zfV+kxg+-EKA!EN<EoTYO0^S<Vg^c|IwOln^3-~6pG0OAv*K*e|r?6&o7VW9wULY{p zgwe8Iu!eUbV+w~jLkY-Sp&FirjN%NnEF~NZgll+eSQaudGNdr4u;z%=@<CL9Nx>S1 zg^UXsYx!$9QaB|UK>BMqQn(};#2IP@Qn(k0Ld4S;!K%d|atj$#cv5&<nLw(*;zBh% zlix7Pvx|fLus~w65R+;>*yPy^bD3&IYD8u;q=?iA&t{m*Tq{~4nIhWE$jDG4Sp%kP z_!dajh%OLY$gq%+k)a0R2f-QvkRL#9Vq_?hULcJ{PE4F(A!DsrjhG8VtU#@JiDZg+ zjd(K?BZJTcrb3w#mIW*e8ERN17-}U-BsZU6y3WWbGkF1vEk9dvMrlcA&Mmg0+>)Zy z)X8^Pd?y>So?(=k{Et<cQE{>oTLh!&<Z8BjB@a-<@d_|<F!3;oF!C_*Fmf>oF!C_U zF>x?*FiJ47FiqxQ*Y+@HU|`T>^7GT=EK+1(U?>s+5quy*9YlzM2q6%`0wN?p1V~Gf ODTsw2%qCZ}s{sJdmwiP5 delta 488 zcmX@izmT6dl$V!_fq{WxR;pU!go(Tp8O<in_mJmUz*)nQ!XnAAkg;E_ma~Lw0e21O zLdJf9TCN(d1w4~w80GnSYq@KfQ&_V(i<Z@JFW{T(#b{~HU&FhQF@-~%p@boeae+V$ z&q79VhFX>q!39D!JT)u}nHU*Tm{VADglqXAD!?TF<Xw#N>LN89DV&lFX^dbo(Hf2v zE|C2xJSn`bOp**RGbb@AvWwL)E@WIFK3RiFm63n4ACqLgM2cuLBO^nJL=Bj(;aebC zBf3CzA;UsOMur-MllW@{Ku!WVk&&T9YJn6MIWcjDg^aagHDWFdu>!T?B@!v(HR8=o zj0_W)3fW3n7O*U2s9}|0sFf&@NRg<K0Lg5A#&n*MQF`(=7Td|ptUl~)#Tlg~nK_gF zSkE#_O_pI(W>lE$z!t%1GI=IjzMKmvz<C9jIGA`CMHqRQ1Q>Z3<(N1aIT$6FSePek zv1@ynF)%P_GWq#wauz8tFfbH}fCxSip#~!4K!gy8U;z<gAOfVN$OObf5T=vova10A DYX)ZL diff --git a/unitgrade_private2/docker_helpers.py b/unitgrade_private2/docker_helpers.py index 965ddf5..a91e4ba 100644 --- a/unitgrade_private2/docker_helpers.py +++ b/unitgrade_private2/docker_helpers.py @@ -26,13 +26,15 @@ def docker_run_token_file(Dockerfile_location, host_tmp_dir, student_token_file, """ # A bunch of tests. This is going to be great! assert os.path.exists(Dockerfile_location) - start = time.time() with open(student_token_file, 'rb') as f: results = pickle.load(f) sources = results['sources'][0] + if os.path.exists(host_tmp_dir): + shutil.rmtree(host_tmp_dir) + with io.BytesIO(sources['zipfile']) as zb: with zipfile.ZipFile(zb) as zip: zip.extractall(host_tmp_dir) @@ -43,7 +45,6 @@ def docker_run_token_file(Dockerfile_location, host_tmp_dir, student_token_file, else: gscript = instructor_grade_script - # rel_location = student_grade_script = host_tmp_dir + "/" + sources['name'] + "/" + sources['report_relative_location'] instructor_grade_script = os.path.dirname(student_grade_script) + "/"+os.path.basename(gscript) shutil.copy(gscript, instructor_grade_script) -- GitLab