From b6ead38d702e0b3d2161b1e5e4a05a17fe173ae5 Mon Sep 17 00:00:00 2001
From: Tue Herlau <tuhe@dtu.dk>
Date: Thu, 18 Mar 2021 02:13:52 +0100
Subject: [PATCH] Update to version 1.3: Added progress bars

---
 unitgrade/__init__.py                         |   3 +-
 unitgrade/__pycache__/__init__.cpython-38.pyc | Bin 1080 -> 1125 bytes
 .../__pycache__/unitgrade.cpython-38.pyc      | Bin 11875 -> 13332 bytes
 .../unitgrade_helpers.cpython-38.pyc          | Bin 5764 -> 6555 bytes
 unitgrade/__pycache__/version.cpython-38.pyc  | Bin 167 -> 166 bytes
 unitgrade/unitgrade.py                        |  70 +++++++++++++--
 unitgrade/unitgrade_helpers.py                |  84 ++++++++++++++----
 unitgrade/version.py                          |   2 +-
 8 files changed, 131 insertions(+), 28 deletions(-)

diff --git a/unitgrade/__init__.py b/unitgrade/__init__.py
index d7f3cb7..605eedf 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
GIT binary patch
delta 138
zcmdnN@sxu%l$V!_fq{Wx&$OU~<cYkpj6oB%-8pi(qquW<qIfnIEoNk7n=HoE%qTc{
z7Sk;$F$M;PVz35A4kjK(9!4eRB54K&22Hlfea!L-w>X^>3rb3hGV{`J@i``!WR|4{
o6y>KEr4|=!GKOgKPTs^E#-_l)z)++(nT17{orQ~=gO!5|0M=X|*Z=?k

delta 76
zcmaFLv4ev*l$V!_fq{V`(MKY|Z6dEMqt`@j_l;GH85vn8%P}=G3QS(abc<1RaxJq2
hBg^Dz%<`<7j3Jubldm#|O*UoGWM}5$=3wRE0sz1(5fK0Y

diff --git a/unitgrade/__pycache__/unitgrade.cpython-38.pyc b/unitgrade/__pycache__/unitgrade.cpython-38.pyc
index e2fdc61918eb16cd7180fda4503cdcf6bc966acf..5601367584ffb2dd33f99e057b47d73958c6bf42 100644
GIT binary patch
delta 4860
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
JbMSHS0008JKurJu

delta 3443
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

diff --git a/unitgrade/__pycache__/unitgrade_helpers.cpython-38.pyc b/unitgrade/__pycache__/unitgrade_helpers.cpython-38.pyc
index 29fcc07326d80d9125066f6a3ef9ee265e53a622..d7f1dcc24f9053facd9ab6146edd241ac30a3ca1 100644
GIT binary patch
delta 3493
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

delta 2618
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
QhD}t2Q2+utxD+|q08}?@B>(^b

diff --git a/unitgrade/__pycache__/version.cpython-38.pyc b/unitgrade/__pycache__/version.cpython-38.pyc
index d8b1642642aadb9136268b7e9bd9759eb8164c3a..79890100982f99dfa74eaaa28454b969f4f9d8fb 100644
GIT binary patch
delta 31
mcmZ3^xQvlEl$V!_fq{Wx?slI9(TTh|Y^(-)hI+;m9nt}5^#<qw

delta 32
ncmZ3+xSWwUl$V!_fq{YH>q)r;v5CAo>}&>lhI)p{6YbLhb_EBA

diff --git a/unitgrade/unitgrade.py b/unitgrade/unitgrade.py
index df8a7f8..e57ccbc 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 85b51ff..5edd3ee 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 48fef32..acf3be3 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
-- 
GitLab