From a230d4c9381a9585dff73130b482f25966d62916 Mon Sep 17 00:00:00 2001
From: Tue Herlau <tuhe@dtu.dk>
Date: Mon, 30 Aug 2021 17:50:12 +0200
Subject: [PATCH] updates

---
 .../__pycache__/unitgrade2.cpython-38.pyc     | Bin 26946 -> 29204 bytes
 .../unitgrade_helpers2.cpython-38.pyc         | Bin 6825 -> 6959 bytes
 unitgrade2/unitgrade2.py                      | 162 +++++++++++-------
 unitgrade2/unitgrade_helpers2.py              | 131 +-------------
 4 files changed, 110 insertions(+), 183 deletions(-)

diff --git a/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc b/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc
index a70e7b1190a2294f68b5e051feddd744b6c88a79..3492c9eca22889350f2a5707f0579dc18533a9c0 100644
GIT binary patch
delta 15097
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

delta 13026
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
BLwf)K

diff --git a/unitgrade2/__pycache__/unitgrade_helpers2.cpython-38.pyc b/unitgrade2/__pycache__/unitgrade_helpers2.cpython-38.pyc
index 5f1fcf25687cedd7203740db7a57b848a3ce5503..efd8ce6f58cc6a9f0d3cd6104d4cbe6e9982131d 100644
GIT binary patch
delta 2663
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

delta 2518
zcmZ2)w$fBPl$V!_fq{YH&wq`?V<rp?k3k${%+0{S;K0DZP#iQ-JByP&hc}l$il32T
z;w*#u6owSW9I+^|RG}>K6pj?mRG}=16s}(8D9KdGEU6Uk6rNP+6vh<Z6uw?oMh37L
ze~LhgV2V&LGn6l!B9J12#1~BwND)Kgi>C;rNI>{0swq+_(k+ZpGAS}CvMmfzrn2q~
zDRL?DEet90sXSS7&CF5q?hGjkDT*x&DT=8)Sqja}QHm*QDat7-EsRl0DU87kn(C9w
z7!x_zeJg`YGfPr8UtwIz%qX(CiZzajO@e`ep-5u#J@#Nmlg&mPUl|!CCu?wdv8jT@
zl_pnkb!o|gm<k{QBFMnN5G9<Mo|j*g8lRh4T%4Jg9-o$(lUk%US%=$D0AeCYxeNmX
z!!7a24jkf)`jcC^)ftT^ujQ6xbeepe`-Cz`nI=<_6i5wYW)X-BAwar{WEmJ3+9z+~
z(Ph+_{D4PETL-Kx1EK<~7NqPJYiU7Za&{{yrr1F7$0WqaHMx<`d2#`7vZgIa4_kJ5
zVo`eWO2%89@$tzyiN(e7@eor$1{C=-FfjB?{=@5U31Z#ig{nzT&B=kO1KSn^^G}g4
z0|P@h$U-It1_lnsBHzgdybhC(@F^LyFfcGMLvb<4!6gjM3=0@*7#A|uGL<mZFlI3?
zU|Gn}%vj4@!<5BZ!<@x7*`8lig1yKO<Whn3)RK_Y;u7b?;#9xH+|=U96Zut`S&9NC
z@8Fj+i4rJDEiOq;EKZHjO)bgDPbn?}y9w+%uvelO7#Jpi9L50(2}T}9zR8jTy7eI-
zmxqFgFc1M!RTK_lfpvljut8umA{ZDL`axzuJqO~4B=dj_gkq4t*cliYoI$EYm>C#S
z7*m*X7;+hFc^DZ=m=-WEWT@pWVaZ}mVNPL5VePGFs^v>zTfmmWp2E?>P{X%?eIbJj
zLo+B$_)9oy_)|E=8ESaM8JZax8ESYJaHeoAWULh^;Yi`G5rB#^WO3CBmT=cFr0_H|
z6?N1wWbvdhWOEjE)Ci{Vf_UN#<_xuhB^)VyHG<*{H5^4VQuy<jQUscr7#Zr*8ES=6
z1Z#y$co*<4WT+L%WT+J_;cI576)WMdVaO6_W^`egz!+N=!&ECC!&EC#E14oxD^<e?
zw&GQdc#5z%Lk(j#bJ459Lp5Rx1ZyM~GS*7hNMs4s%A|-u<z>>C#29L2OE_v|Qv^~(
zdzly+QW$F_;)PS{K@`YRxfI3}u^iD{v08aXh8o!t@py?E`4sUI@f3+(rUjBU(hC`D
z6;dQ?6>Ai-q-vE)q-zwiWSSXkm0>I}E!)f}&H!e~fmk55*-R4{i#tl>n;2`9QW&#Y
zCNLH)D^aMCtWg$WNRg@+VW?4(W@u*2V@hGHRj!c)Nz};I$fQWmVXYCZ5w~HeQLd4Q
z7pYMKyH^I{quC5|nQB#1WNRd{L}oLj$kj;BW|+%Tt6IWQqmm+DBU~a~qteVM0dk5e
zh!$ZGXQ)wyL|6$&iYz!fg%&8L)GHtiL@|sxooOK>BSYb#5~Ui^X2uxiTD4lGTICXj
z66G4DX2xd5S|u=B8O&C$QL15x7p_sRVTc!jB{Oh(Q3R(K1xWPPh)96)mwJtQijovV
ztwxP{eT_znk_dwYL#-ybNMxwhD&a^`1tqQ&HF1U-ts0FK^=2k<h7=7jPcuam%+rGS
zr3T~&?G%9$l^TXD5m1n)$cZz6dBPwb#1l2jHA-nrHKHjxbC_ziYqV>G(wKr7G<E%o
z7$-jzu9ATiaygK42b|@=xeZjv&6!*-Vmx`PND7-<kp=?;!(=^C9Y*)b(W0_4FJCb*
zFr-8B-3NP3)*?`zD{=wlUDwI|qDq|V3=9mv81;+9C+`wXum|zLM%-d7E=pryV9;c|
z#avucq{&p|2GUaqBEXh|36KkJaoFS}<|d^i+G#OPjugvbRGoZG%wN?MR2*>>mlUOy
zWag$8<$*NnfrwO)1PfR&VzQOEAvd^ifx4(#Tv{^|qzY7A+~O^aFUc&)NsTWk%FHXd
z#avXHcZ&m5gcS#s<`pGPJ|k}EBndK(EwP{=H7}*e2c#8Lh!+)s+^$n(0}?>gJxU<1
zB8X515h@@;b+WyLjDS3dsRklIm3mS2<Z=mxda!TpK^8FO`P^bJi7zfmEGoIhTAZ9;
zlzNLTCqFSIwdfXOUJ<B31BYxBM`3(MW=cwG-YwQ*P(2yNRtWY&5vYENVk(R;0(tKi
zb5UyXEygNnM1XB5HoV1DY;=pMy!aMVO35wu+|2sC(vsBTTkOTD$@zIH#ZfE;IYl`|
zb|6QBLmy0lqM*oxfq`Kfs4y)7#WXVyBL^cNqXeS>BNrnNBO9X#BOjv}6AL2?BL^c3
z6B~;J6AP0FBNHPR6C1MtSOps+3nLFR8>0{ti#r=57n2s75~IlE9!VWzkkOiKetv#_
zZf=^~AkxjvEyOLvO;h<6YhGz?L1mFA$VH&Sz6exv71d0BBdKe0i=#9zvjh}bMJ*tC
zP;00N6stvMAg(^h&m4(G=>>^J#i_TLiz|zZ8YVkSS;uODWOx!&QsNVf(o1tw^Gd*x
zS>y!L28!TYd?Be7;94%IG%qi;2wa=q;sRBBKHwNB0@XO+FhR;JkieRJTS}Rkk%w{e
Xe<^u>X09*}HX#-vb_PZUMuvX?rY=bK

diff --git a/unitgrade2/unitgrade2.py b/unitgrade2/unitgrade2.py
index 08c9d48..c094340 100644
--- a/unitgrade2/unitgrade2.py
+++ b/unitgrade2/unitgrade2.py
@@ -45,12 +45,13 @@ class Logger(object):
         pass
 
 class Capturing(list):
-    def __init__(self, *args, unmute=False, **kwargs):
+    def __init__(self, *args, stdout=None, unmute=False, **kwargs):
+        self._stdout = stdout
         self.unmute = unmute
         super().__init__(*args, **kwargs)
 
     def __enter__(self, capture_errors=True): # don't put arguments here.
-        self._stdout = sys.stdout
+        self._stdout = sys.stdout if self._stdout == None else self._stdout
         self._stringio = StringIO()
         if self.unmute:
             sys.stdout = Logger(self._stringio)
@@ -70,6 +71,20 @@ class Capturing(list):
         if self.capture_errors:
             sys.sterr = self._sterr
 
+class Capturing2(Capturing):
+    def __exit__(self, *args):
+        lines = self._stringio.getvalue().splitlines()
+        txt = "\n".join(lines)
+        numbers = extract_numbers(txt)
+        self.extend(lines)
+        del self._stringio    # free up some memory
+        sys.stdout = self._stdout
+        if self.capture_errors:
+            sys.sterr = self._sterr
+
+        self.output = txt
+        self.numbers = numbers
+
 
 class QItem(unittest.TestCase):
     title = None
@@ -313,12 +328,13 @@ class Report():
         modules = os.path.normpath(relative_path[:-3]).split(os.sep)
         return root_dir, relative_path, modules
 
-
     def __init__(self, strict=False, payload=None):
         working_directory = os.path.abspath(os.path.dirname(self._file()))
 
         self.wdir, self.name = setup_dir_by_class(self, working_directory)
         # self.computed_answers_file = os.path.join(self.wdir, self.name + "_resources_do_not_hand_in.dat")
+        for (q,_) in self.questions:
+            q.nL = self.nL # Set maximum line length.
 
         if payload is not None:
             self.set_payload(payload, strict=strict)
@@ -360,28 +376,6 @@ class Report():
         for q, _ in self.questions:
             q._cache = payloads[q.__qualname__]
 
-            # for item in q.items:
-            #     if q.name not in payloads or item.name not in payloads[q.name]:
-            #         s = f"> Broken resource dictionary submitted to unitgrade for question {q.name} and subquestion {item.name}. Framework will not work."
-            #         if strict:
-            #             raise Exception(s)
-            #         else:
-            #             print(s)
-            #     else:
-            #         item._correct_answer_payload = payloads[q.name][item.name]['payload']
-            #         item.estimated_time = payloads[q.name][item.name].get("time", 1)
-            #         q.estimated_time = payloads[q.name].get("time", 1)
-            #         if "precomputed" in payloads[q.name][item.name]: # Consider removing later.
-            #             item._precomputed_payload = payloads[q.name][item.name]['precomputed']
-            #         try:
-            #             if "title" in payloads[q.name][item.name]: # can perhaps be removed later.
-            #                 item.title = payloads[q.name][item.name]['title']
-            #         except Exception as e: # Cannot set attribute error. The title is a function (and probably should not be).
-            #             pass
-            #             # print("bad", e)
-        # self.payloads = payloads
-
-
 def rm_progress_bar(txt):
     # More robust version. Apparently length of bar can depend on various factors, so check for order of symbols.
     nlines = []
@@ -404,10 +398,9 @@ def extract_numbers(txt):
     all = [float(a) if ('.' in a or "e" in a) else int(a) for a in all]
     if len(all) > 500:
         print(txt)
-        raise Exception("unitgrade.unitgrade.py: Warning, many numbers!", len(all))
+        raise Exception("unitgrade.unitgrade.py: Warning, too many numbers!", len(all))
     return all
 
-
 class ActiveProgress():
     def __init__(self, t, start=True, title="my progress bar",show_progress_bar=True):
         self.t = t
@@ -459,8 +452,9 @@ class ActiveProgress():
 
 from unittest.suite import _isnotsuite
 
-class MySuite(unittest.suite.TestSuite): # Not sure we need this one anymore.
-    pass
+# class MySuite(unittest.suite.TestSuite): # Not sure we need this one anymore.
+#     raise Exception("no suite")
+#     pass
 
 def instance_call_stack(instance):
     s = "-".join(map(lambda x: x.__name__, instance.__class__.mro()))
@@ -616,8 +610,11 @@ class UTextResult(unittest.TextTestResult):
         n = UTextResult.number
 
         item_title = self.getDescription(test)
-        item_title = item_title.split("\n")[0]
-
+        # item_title = item_title.split("\n")[0]
+        item_title = test.shortDescription() # Better for printing (get from cache).
+        if item_title == None:
+            # For unittest framework where getDescription may return None.
+            item_title = self.getDescription(test)
         # test.countTestCases()
         self.item_title_print = "*** q%i.%i) %s" % (n + 1, j + 1, item_title)
         estimated_time = 10
@@ -632,7 +629,7 @@ class UTextResult(unittest.TextTestResult):
 
     def _setupStdout(self):
         if self._previousTestClass == None:
-            total_estimated_time = 2
+            total_estimated_time = 1
             if hasattr(self.__class__, 'q_title_print'):
                 q_title_print = self.__class__.q_title_print
             else:
@@ -688,7 +685,10 @@ def cache(foo, typed=False):
     """
     maxsize = None
     def wrapper(self, *args, **kwargs):
-        key = self.cache_id() + ("cache", _make_key(args, kwargs, typed))
+        key = (self.cache_id(), ("@cache", foo.__name__, _make_key(args, kwargs, typed)) )
+        # key = (self.cache_id(), '@cache')
+        # if self._cache_contains[key]
+
         if not self._cache_contains(key):
             value = foo(self, *args, **kwargs)
             self._cache_put(key, value)
@@ -700,15 +700,56 @@ def cache(foo, typed=False):
 
 class UTestCase(unittest.TestCase):
     _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache.
-    _cache = None  # Read-only cache.
-    _cache2 = None # User-written cache
+    _cache = None  # Read-only cache. Ensures method always produce same result.
+    _cache2 = None  # User-written cache.
+
+    def capture(self):
+        return Capturing2(stdout=self._stdout)
+
+    @classmethod
+    def question_title(cls):
+        """ Return the question title """
+        return cls.__doc__.strip().splitlines()[0].strip() if cls.__doc__ != None else cls.__qualname__
 
     @classmethod
     def reset(cls):
+        print("Warning, I am not sure UTestCase.reset() is needed anymore and it seems very hacky.")
         cls._outcome = None
         cls._cache = None
         cls._cache2 = None
 
+    def _callSetUp(self):
+        self._stdout = sys.stdout
+        import io
+        sys.stdout = io.StringIO()
+        super().setUp()
+        # print("Setting up...")
+
+    def _callTearDown(self):
+        sys.stdout = self._stdout
+        super().tearDown()
+        # print("asdfsfd")
+
+    def shortDescriptionStandard(self):
+        sd = super().shortDescription()
+        if sd == None:
+            sd = self._testMethodName
+        return sd
+
+    def shortDescription(self):
+        # self._testMethodDoc.strip().splitlines()[0].strip()
+        sd = self.shortDescriptionStandard()
+        title = self._cache_get(  (self.cache_id(), 'title'), sd )
+        return title if title != None else sd
+
+    @property
+    def title(self):
+        return self.shortDescription()
+
+    @title.setter
+    def title(self, value):
+        self._cache_put((self.cache_id(), 'title'), value)
+
     def _get_outcome(self):
         if not (self.__class__, '_outcome') or self.__class__._outcome == None:
             self.__class__._outcome = {}
@@ -716,37 +757,31 @@ class UTestCase(unittest.TestCase):
 
     def _callTestMethod(self, testMethod):
         t = time.time()
+        self._ensure_cache_exists() # Make sure cache is there.
+        if self._testMethodDoc != None:
+            # Ensure the cache is eventually updated with the right docstring.
+            self._cache_put((self.cache_id(), 'title'), self.shortDescriptionStandard() )
+        # Fix temp cache here (for using the @cache decorator)
+        self._cache2[ (self.cache_id(), 'assert') ] = {}
+
         res = testMethod()
         elapsed = time.time() - t
-        # if res == None:
-        #     res = {}
-        # res['time'] = elapsed
-        sd = self.shortDescription()
-        self._cache_put( (self.cache_id(), 'title'), self._testMethodName if sd == None else sd)
-        # self._test_fun_output = res
+        # self._cache_put( (self.cache_id(), 'title'), self.shortDescription() )
+
         self._get_outcome()[self.cache_id()] = res
         self._cache_put( (self.cache_id(), "time"), elapsed)
 
-
     # This is my base test class. So what is new about it?
     def cache_id(self):
         c = self.__class__.__qualname__
         m = self._testMethodName
         return (c,m)
 
-    def unique_cache_id(self):
-        k0 = self.cache_id()
-        key = ()
-        for i in itertools.count():
-            key = k0 + (i,)
-            if not self._cache2_contains(key):
-                break
-        return key
-
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self._load_cache()
-        self.cache_indexes = defaultdict(lambda: 0)
+        self._assert_cache_index = 0
+        # self.cache_indexes = defaultdict(lambda: 0)
 
     def _ensure_cache_exists(self):
         if not hasattr(self.__class__, '_cache') or self.__class__._cache == None:
@@ -766,17 +801,22 @@ class UTestCase(unittest.TestCase):
         self._ensure_cache_exists()
         return key in self.__class__._cache
 
-    def _cache2_contains(self, key):
-        self._ensure_cache_exists()
-        return key in self.__class__._cache2
+    def wrap_assert(self, assert_fun, first, *args, **kwargs):
+        key = (self.cache_id(), 'assert')
+        if not self._cache_contains(key):
+            print("Warning, framework missing", key)
+        cache = self._cache_get(key, {})
+        id = self._assert_cache_index
+        if not id in cache:
+            print("Warning, framework missing cache index", key, "id =", id)
+        _expected = cache.get(id, first)
+        assert_fun(first, _expected, *args, **kwargs)
+        cache[id] = first
+        self._cache_put(key, cache)
+        self._assert_cache_index += 1
 
     def assertEqualC(self, first: Any, msg: Any = ...) -> None:
-        id = self.unique_cache_id()
-        if not self._cache_contains(id):
-            print("Warning, framework missing key", id)
-
-        self.assertEqual(first, self._cache_get(id, first), msg)
-        self._cache_put(id, first)
+        self.wrap_assert(self.assertEqual, first, msg)
 
     def _cache_file(self):
         return os.path.dirname(inspect.getfile(self.__class__) ) + "/unitgrade/" + self.__class__.__name__ + ".pkl"
diff --git a/unitgrade2/unitgrade_helpers2.py b/unitgrade2/unitgrade_helpers2.py
index 3562336..8421562 100644
--- a/unitgrade2/unitgrade_helpers2.py
+++ b/unitgrade2/unitgrade_helpers2.py
@@ -5,7 +5,7 @@ import pyfiglet
 from unitgrade2 import Hidden, myround, msum, mfloor, ActiveProgress
 from unitgrade2 import __version__
 import unittest
-from unitgrade2.unitgrade2 import MySuite
+# from unitgrade2.unitgrade2 import MySuite
 from unitgrade2.unitgrade2 import UTextResult
 
 import inspect
@@ -64,53 +64,6 @@ def evaluate_report_student(report, question=None, qitem=None, unmute=None, pass
                                           show_tol_err=show_tol_err)
 
 
-    # try:  # For registering stats.
-    #     import unitgrade_private
-    #     import irlc.lectures
-    #     import xlwings
-    #     from openpyxl import Workbook
-    #     import pandas as pd
-    #     from collections import defaultdict
-    #     dd = defaultdict(lambda: [])
-    #     error_computed = []
-    #     for k1, (q, _) in enumerate(report.questions):
-    #         for k2, item in enumerate(q.items):
-    #             dd['question_index'].append(k1)
-    #             dd['item_index'].append(k2)
-    #             dd['question'].append(q.name)
-    #             dd['item'].append(item.name)
-    #             dd['tol'].append(0 if not hasattr(item, 'tol') else item.tol)
-    #             error_computed.append(0 if not hasattr(item, 'error_computed') else item.error_computed)
-    #
-    #     qstats = report.wdir + "/" + report.name + ".xlsx"
-    #
-    #     if os.path.isfile(qstats):
-    #         d_read = pd.read_excel(qstats).to_dict()
-    #     else:
-    #         d_read = dict()
-    #
-    #     for k in range(1000):
-    #         key = 'run_'+str(k)
-    #         if key in d_read:
-    #             dd[key] = list(d_read['run_0'].values())
-    #         else:
-    #             dd[key] = error_computed
-    #             break
-    #
-    #     workbook = Workbook()
-    #     worksheet = workbook.active
-    #     for col, key in enumerate(dd.keys()):
-    #         worksheet.cell(row=1, column=col+1).value = key
-    #         for row, item in enumerate(dd[key]):
-    #             worksheet.cell(row=row+2, column=col+1).value = item
-    #
-    #     workbook.save(qstats)
-    #     workbook.close()
-    #
-    # except ModuleNotFoundError as e:
-    #     s = 234
-    #     pass
-
     if question is None:
         print("Provisional evaluation")
         tabulate(table_data)
@@ -142,7 +95,12 @@ class UnitgradeTextRunner(unittest.TextTestRunner):
 class SequentialTestLoader(unittest.TestLoader):
     def getTestCaseNames(self, testCaseClass):
         test_names = super().getTestCaseNames(testCaseClass)
-        testcase_methods = list(testCaseClass.__dict__.keys())
+        # testcase_methods = list(testCaseClass.__dict__.keys())
+        ls = []
+        for C in testCaseClass.mro():
+            if issubclass(C, unittest.TestCase):
+                ls = list(C.__dict__.keys()) + ls
+        testcase_methods = ls
         test_names.sort(key=testcase_methods.index)
         return test_names
 
@@ -174,12 +132,12 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa
 
     for n, (q, w) in enumerate(report.questions):
         # q = q()
-        q_hidden = False
+        # q_hidden = False
         # q_hidden = issubclass(q.__class__, Hidden)
         if question is not None and n+1 != question:
             continue
         suite = loader.loadTestsFromTestCase(q)
-        qtitle = q.__name__
+        qtitle = q.question_title() if hasattr(q, 'question_title') else q.__qualname__
         q_title_print = "Question %i: %s"%(n+1, qtitle)
         print(q_title_print, end="")
         q.possible = 0
@@ -193,77 +151,6 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa
         UTextResult.number = n
 
         res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite)
-        # res = UTextTestRunner(verbosity=2, resultclass=unittest.TextTestResult).run(suite)
-        z = 234
-        # for j, item in enumerate(q.items):
-        #     if qitem is not None and question is not None and j+1 != qitem:
-        #         continue
-        #
-        #     if q_with_outstanding_init is not None: # check for None bc. this must be called to set titles.
-        #         # if not item.question.has_called_init_:
-        #         start = time.time()
-        #
-        #         cc = None
-        #         if show_progress_bar:
-        #             total_estimated_time = q.estimated_time # Use this. The time is estimated for the q itself.  # sum( [q2.estimated_time for q2 in q_with_outstanding_init] )
-        #             cc = ActiveProgress(t=total_estimated_time, title=q_title_print)
-        #         from unitgrade import Capturing # DON'T REMOVE THIS LINE
-        #         with eval('Capturing')(unmute=unmute):  # Clunky import syntax is required bc. of minify issue.
-        #             try:
-        #                 for q2 in q_with_outstanding_init:
-        #                     q2.init()
-        #                     q2.has_called_init_ = True
-        #
-        #                 # item.question.init()  # Initialize the question. Useful for sharing resources.
-        #             except Exception as e:
-        #                 if not passall:
-        #                     if not silent:
-        #                         print(" ")
-        #                         print("="*30)
-        #                         print(f"When initializing question {q.title} the initialization code threw an error")
-        #                         print(e)
-        #                         print("The remaining parts of this question will likely fail.")
-        #                         print("="*30)
-        #
-        #         if show_progress_bar:
-        #             cc.terminate()
-        #             sys.stdout.flush()
-        #             print(q_title_print, end="")
-        #
-        #         q_time =np.round(  time.time()-start, 2)
-        #
-        #         print(" "* max(0,nL - len(q_title_print) ) + (" (" + str(q_time) + " seconds)" if q_time >= 0.1 else "") ) # if q.name in report.payloads else "")
-        #         print("=" * nL)
-        #         q_with_outstanding_init = None
-        #
-        #     # item.question = q # Set the parent question instance for later reference.
-        #     item_title_print = ss = "*** q%i.%i) %s"%(n+1, j+1, item.title)
-        #
-        #     if show_progress_bar:
-        #         cc = ActiveProgress(t=item.estimated_time, title=item_title_print)
-        #     else:
-        #         print(item_title_print + ( '.'*max(0, nL-4-len(ss)) ), end="")
-        #     hidden = issubclass(item.__class__, Hidden)
-        #     # if not hidden:
-        #     #     print(ss, end="")
-        #     # sys.stdout.flush()
-        #     start = time.time()
-        #
-        #     (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent)
-        #     q_[j] = {'w': item.weight, 'possible': possible, 'obtained': current, 'hidden': hidden, 'computed': str(item._computed_answer), 'title': item.title}
-        #     tsecs = np.round(time.time()-start, 2)
-        #     if show_progress_bar:
-        #         cc.terminate()
-        #         sys.stdout.flush()
-        #         print(item_title_print + ('.' * max(0, nL - 4 - len(ss))), end="")
-        #
-        #     if not hidden:
-        #         ss = "PASS" if current == possible else "*** FAILED"
-        #         if tsecs >= 0.1:
-        #             ss += " ("+ str(tsecs) + " seconds)"
-        #         print(ss)
-
-        # ws, possible, obtained = upack(q_)
 
         possible = res.testsRun
         obtained = len(res.successes)
-- 
GitLab