diff --git a/unitgrade/__init__.py b/unitgrade/__init__.py index d7f3cb7b866d6bd68c0370f98b5b5162bf5490fe..605eedf9521bbb549018417e8ade8e8c9e7a6f40 100644 --- a/unitgrade/__init__.py +++ b/unitgrade/__init__.py @@ -1,5 +1,6 @@ from unitgrade.version import __version__ import os + # DONT't import stuff here since install script requires __version__ def cache_write(object, file_name, verbose=True): @@ -28,4 +29,4 @@ def cache_read(file_name): else: return None -from unitgrade.unitgrade import Hidden, myround, mfloor, msum +from unitgrade.unitgrade import Hidden, myround, mfloor, msum, Capturing, ActiveProgress diff --git a/unitgrade/__pycache__/__init__.cpython-38.pyc b/unitgrade/__pycache__/__init__.cpython-38.pyc index 57e9c8499c7c715af060fa5dac373ccf7e32da49..dc097231ab1cc676f96b6e553c3f3a9d29cccfee 100644 Binary files a/unitgrade/__pycache__/__init__.cpython-38.pyc and b/unitgrade/__pycache__/__init__.cpython-38.pyc differ diff --git a/unitgrade/__pycache__/unitgrade.cpython-38.pyc b/unitgrade/__pycache__/unitgrade.cpython-38.pyc index e2fdc61918eb16cd7180fda4503cdcf6bc966acf..5601367584ffb2dd33f99e057b47d73958c6bf42 100644 Binary files a/unitgrade/__pycache__/unitgrade.cpython-38.pyc and b/unitgrade/__pycache__/unitgrade.cpython-38.pyc differ diff --git a/unitgrade/__pycache__/unitgrade_helpers.cpython-38.pyc b/unitgrade/__pycache__/unitgrade_helpers.cpython-38.pyc index 29fcc07326d80d9125066f6a3ef9ee265e53a622..d7f1dcc24f9053facd9ab6146edd241ac30a3ca1 100644 Binary files a/unitgrade/__pycache__/unitgrade_helpers.cpython-38.pyc and b/unitgrade/__pycache__/unitgrade_helpers.cpython-38.pyc differ diff --git a/unitgrade/__pycache__/version.cpython-38.pyc b/unitgrade/__pycache__/version.cpython-38.pyc index d8b1642642aadb9136268b7e9bd9759eb8164c3a..79890100982f99dfa74eaaa28454b969f4f9d8fb 100644 Binary files a/unitgrade/__pycache__/version.cpython-38.pyc and b/unitgrade/__pycache__/version.cpython-38.pyc differ diff --git a/unitgrade/unitgrade.py b/unitgrade/unitgrade.py index df8a7f815e4fdd3cc4a09d6db5f906b9fd633a58..e57ccbc0b1637ecfaa96a429a631809113040604 100644 --- a/unitgrade/unitgrade.py +++ b/unitgrade/unitgrade.py @@ -11,6 +11,9 @@ from io import StringIO import collections import inspect import re +import threading +import tqdm +import time myround = lambda x: np.round(x) # required. msum = lambda x: sum(x) @@ -71,8 +74,11 @@ class QItem(unittest.TestCase): title = None testfun = None tol = 0 - + estimated_time = 0.42 + _precomputed_payload = None _computed_answer = None # Internal helper to later get results. + # _precomputed_payload = None + def __init__(self, working_directory=None, correct_answer_payload=None, question=None, *args, **kwargs): if self.tol > 0 and self.testfun is None: self.testfun = self.assertL2Relative @@ -82,6 +88,8 @@ class QItem(unittest.TestCase): self.name = self.__class__.__name__ self._correct_answer_payload = correct_answer_payload self.question = None + # self.a = "not set" + super().__init__(*args, **kwargs) if self.title is None: self.title = self.name @@ -105,7 +113,14 @@ class QItem(unittest.TestCase): print(f"Element-wise differences {diff.tolist()}") self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol}") - def precomputed_resources(self): + # def set_precomputed_payload(self, payload): + # self.a = "blaaah" + # self._precomputed_payload = payload + + def precomputed_payload(self): + return self._precomputed_payload + + def precompute_payload(self): # Pre-compute resources to include in tests (useful for getting around rng). pass @@ -128,8 +143,8 @@ class QItem(unittest.TestCase): correct = self._correct_answer_payload try: - if unmute: - print("\n") + if unmute: # Required to not mix together print stuff. + print("") computed = self.compute_answer(unmute=unmute) except Exception as e: if not passall: @@ -190,8 +205,8 @@ class QPrintItem(QItem): def process_output(self, res, txt, numbers): return (res, txt) - def compute_local(self): - pass + # def compute_local(self): # Dunno + # pass def compute_answer(self, unmute=False): with Capturing(unmute=unmute) as output: @@ -215,6 +230,7 @@ class QuestionGroup(metaclass=OrderedClassMembers): items = None partially_scored = False t_init = 0 # Time spend on initialization (placeholder; set this externally). + estimated_time = 0.42 def __init__(self, *args, **kwargs): self.name = self.__class__.__name__ @@ -224,6 +240,11 @@ class QuestionGroup(metaclass=OrderedClassMembers): for gt in members: self.items.append( (gt, 1) ) self.items = [(I(question=self), w) for I, w in self.items] + self.has_called_init_ = False + + def init(self): + # Can be used to set resources relevant for this question instance. + pass class Report(): title = "report title" @@ -239,9 +260,13 @@ class Report(): import time qs = [] # Has to accumulate to new array otherwise the setup/evaluation steps cannot be run in sequence. for k, (Q, w) in enumerate(self.questions): + # print(k, Q) start = time.time() q = (Q(working_directory=self.wdir), w) q[0].t_init = time.time() - start + # if time.time() -start > 0.2: + # raise Exception(Q, "Question takes to long to initialize. Use the init() function to set local variables instead") + # print(time.time()-start) qs.append(q) self.questions = qs # self.questions = [(Q(working_directory=self.wdir),w) for Q,w in self.questions] @@ -257,6 +282,7 @@ class Report(): else: print(s) + def set_payload(self, payloads, strict=False): for q, _ in self.questions: for item, _ in q.items: @@ -268,7 +294,9 @@ class Report(): print(s) else: item._correct_answer_payload = payloads[q.name][item.name]['payload'] - if "precomputed" in payloads[q.name][item.name]: + item.estimated_time = payloads[q.name][item.name]['time'] + q.estimated_time = payloads[q.name]['time'] + if "precomputed" in payloads[q.name][item.name]: # Consider removing later. item._precomputed_payload = payloads[q.name][item.name]['precomputed'] self.payloads = payloads @@ -297,3 +325,31 @@ def extract_numbers(txt): print(txt) raise Exception("unitgrade.unitgrade.py: Warning, many numbers!", len(all)) return all + + +class ActiveProgress(): + def __init__(self, t, start=True, title="my progress bar"): + self.t = t + self._running = False + self.title = title + if start: + self.start() + + def start(self): + self._running = True + self.thread = threading.Thread(target=self.run, args=(10,)) + self.thread.start() + + def terminate(self): + self._running = False + self.thread.join() + sys.stdout.flush() + + def run(self, n): + dt = 0.1 + + n = int(np.round(self.t/dt)) + for _ in tqdm.tqdm(range(n), file=sys.stdout, position=0, leave=False, desc=self.title, ncols=100, bar_format='{l_bar}{bar}| [{elapsed}<{remaining}]'): #, unit_scale=dt, unit='seconds'): + if not self._running: + break + time.sleep(dt) \ No newline at end of file diff --git a/unitgrade/unitgrade_helpers.py b/unitgrade/unitgrade_helpers.py index 85b51ff42d9d74a4379dd0cd02a935f231e11ede..5edd3ee7520abdb46fa992f516f78ef6f670b102 100644 --- a/unitgrade/unitgrade_helpers.py +++ b/unitgrade/unitgrade_helpers.py @@ -2,7 +2,8 @@ import numpy as np from tabulate import tabulate from datetime import datetime import pyfiglet -from unitgrade import Hidden, myround, msum, mfloor +from unitgrade import Hidden, myround, msum, mfloor, ActiveProgress +# import unitgrade from unitgrade import __version__ # from unitgrade.unitgrade import Hidden @@ -12,6 +13,10 @@ import inspect import os import argparse import sys +import time +import threading # don't import Thread bc. of minify issue. +import tqdm # don't do from tqdm import tqdm because of minify-issue +#from threading import Thread # This import presents a problem for the minify-code compression tool. parser = argparse.ArgumentParser(description='Evaluate your report.', epilog="""Example: To run all tests in a report: @@ -62,8 +67,8 @@ def evaluate_report_student(report, question=None, qitem=None, unmute=None, pass unmute = args.unmute if passall is None: passall = args.passall - # print(passall) - results, table_data = evaluate_report(report, question=question, qitem=qitem, verbose=False, passall=passall, show_expected=args.showexpected, show_computed=args.showcomputed,unmute=unmute) + + 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) if question is None: print("Provisional evaluation") @@ -85,7 +90,10 @@ def upack(q): h = np.asarray(h) return h[:,0], h[:,1], h[:,2], -def evaluate_report(report, question=None, qitem=None, passall=False, verbose=False, show_expected=False, show_computed=False,unmute=False, show_help_flag=True, silent=False): + + +def evaluate_report(report, question=None, qitem=None, passall=False, verbose=False, show_expected=False, show_computed=False,unmute=False, show_help_flag=True, silent=False, + show_progress_bar=True): from unitgrade.version import __version__ now = datetime.now() ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom") @@ -100,40 +108,71 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa print(f"Loaded answers from: ", report.computed_answers_file, "\n") table_data = [] nL = 80 - + t_start = time.time() score = {} for n, (q, w) in enumerate(report.questions): q_hidden = issubclass(q.__class__, Hidden) + # report.globals = q.globals + # q.globals = report.globals if question is not None and n+1 != question: continue # Don't use f format strings. - print(f"Question {n+1}: {q.title}" + (" (" + str( np.round(report.payloads[q.name].get('time', 0), 2) ) + " seconds)" if q.name in report.payloads else "" ) ) - print("="*nL) + q_title_print = "Question %i: %s"%(n+1, q.title) + print(q_title_print, end="") + # sys.stdout.flush() q.possible = 0 q.obtained = 0 - q_ = {} # Gather score in this class. + # Active progress bar. + for j, (item, iw) in enumerate(q.items): if qitem is not None and question is not None and item is not None and j+1 != qitem: continue + if not q.has_called_init_: + start = time.time() + + cc = None + + if show_progress_bar: + # cc.start() + cc = ActiveProgress(t=q.estimated_time, title=q_title_print) + from unitgrade import Capturing + #eval('from unitgrade import Capturing') + with eval('Capturing')(unmute=unmute): # Clunky import syntax is required bc. of minify issue. + q.init() # Initialize the question. Useful for sharing resources. + if show_progress_bar: + cc.terminate() + print(q_title_print, end="") + + q.has_called_init_ = True + q_time =np.round( time.time()-start, 2) + + + print(" "* max(0,nL - len(q_title_print) ) + " (" + str(q_time) + " seconds)") # if q.name in report.payloads else "") + print("=" * nL) + + item.question = q # Set the parent question instance for later reference. + item_title_print = ss = "*** q%i.%i) %s"%(n+1, j+1, item.title) + + if show_progress_bar: + cc = ActiveProgress(t=item.estimated_time, title=item_title_print) + else: + print(item_title_print + ( '.'*max(0, nL-4-len(ss)) ), end="") - ss = f"*** q{n+1}.{j+1}) {item.title}" - el = nL-4 - if len(ss) < el: - ss += '.'*(el-len(ss)) hidden = issubclass(item.__class__, Hidden) - if not hidden: - print(ss, end="") - sys.stdout.flush() - import time + # if not hidden: + # print(ss, end="") + # sys.stdout.flush() start = time.time() (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent) q_[j] = {'w': iw, 'possible': possible, 'obtained': current, 'hidden': hidden, 'computed': str(item._computed_answer), 'title': item.title} tsecs = np.round(time.time()-start, 2) + if show_progress_bar: + cc.terminate() + sys.stdout.flush() + print(item_title_print + ('.' * max(0, nL - 4 - len(ss))), end="") - # q.possible += possible * iw - # q.obtained += current * iw if not hidden: ss = "PASS" if current == possible else "*** FAILED" ss += " ("+ str(tsecs) + " seconds)" @@ -161,7 +200,14 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa report.obtained = obtained now = datetime.now() dt_string = now.strftime("%H:%M:%S") - print(f"Completed: "+ dt_string) + + dt = int(time.time()-t_start) + minutes = dt//60 + seconds = dt - minutes*60 + plrl = lambda i, s: str(i) + " " + s + ("s" if i != 1 else "") + + print(f"Completed: "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +")") + table_data.append(["Total", ""+str(report.obtained)+"/"+str(report.possible) ]) results = {'total': (obtained, possible), 'details': score} return results, table_data diff --git a/unitgrade/version.py b/unitgrade/version.py index 48fef3235794579e6ccc7e4ec49e8b5188c14101..acf3be3eb86fb072c535366ae4b8e590326c2835 100644 --- a/unitgrade/version.py +++ b/unitgrade/version.py @@ -1 +1 @@ -__version__ = "0.1.2" \ No newline at end of file +__version__ = "0.1.3" \ No newline at end of file