From 7e820f223f4b9ce5cc9c87d56f0e203e26e467b1 Mon Sep 17 00:00:00 2001 From: Tue Herlau <tuhe@dtu.dk> Date: Thu, 2 Sep 2021 15:33:58 +0200 Subject: [PATCH] Updates --- LICENSE | 19 + MANIFEST.in | 1 - README.md | 36 +- cs101courseware_example/instructions.py | 2 +- dist/unitgrade-0.0.2-py3-none-any.whl | Bin 0 -> 17438 bytes dist/unitgrade-0.0.2.tar.gz | Bin 0 -> 17257 bytes dist/unitgrade-0.0.5.tar.gz | Bin 30274 -> 0 bytes pyproject.toml | 6 + setup.py | 43 +- {unitgrade2 => src/unitgrade2}/__init__.py | 3 +- .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 1306 bytes .../__pycache__/unitgrade2.cpython-38.pyc | Bin 0 -> 22713 bytes src/unitgrade2/unitgrade2.py | 705 ++++++++++++++ .../unitgrade2}/unitgrade_helpers2.py | 43 +- src/unitgrade2/version.py | 1 + unitgrade.egg-info/PKG-INFO | 10 - unitgrade.egg-info/SOURCES.txt | 21 - unitgrade.egg-info/dependency_links.txt | 1 - unitgrade.egg-info/requires.txt | 4 - unitgrade.egg-info/top_level.txt | 2 - unitgrade/Report_resources_do_not_hand_in.dat | Bin 64 -> 0 bytes unitgrade/version.py | 2 +- .../__pycache__/__init__.cpython-38.pyc | Bin 1373 -> 0 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 1400 -> 0 bytes .../__pycache__/unitgrade2.cpython-38.pyc | Bin 29204 -> 0 bytes .../unitgrade_helpers2.cpython-38.pyc | Bin 6959 -> 0 bytes unitgrade2/__pycache__/version.cpython-38.pyc | Bin 167 -> 0 bytes unitgrade2/__pycache__/version.cpython-39.pyc | Bin 164 -> 0 bytes unitgrade2/unitgrade/ListQuestion.pkl | Bin 466 -> 0 bytes unitgrade2/unitgrade2.py | 891 ------------------ unitgrade2/version.py | 1 - 31 files changed, 793 insertions(+), 998 deletions(-) create mode 100644 LICENSE delete mode 100644 MANIFEST.in create mode 100644 dist/unitgrade-0.0.2-py3-none-any.whl create mode 100644 dist/unitgrade-0.0.2.tar.gz delete mode 100644 dist/unitgrade-0.0.5.tar.gz create mode 100644 pyproject.toml rename {unitgrade2 => src/unitgrade2}/__init__.py (90%) create mode 100644 src/unitgrade2/__pycache__/__init__.cpython-38.pyc create mode 100644 src/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc create mode 100644 src/unitgrade2/unitgrade2.py rename {unitgrade2 => src/unitgrade2}/unitgrade_helpers2.py (84%) create mode 100644 src/unitgrade2/version.py delete mode 100644 unitgrade.egg-info/PKG-INFO delete mode 100644 unitgrade.egg-info/SOURCES.txt delete mode 100644 unitgrade.egg-info/dependency_links.txt delete mode 100644 unitgrade.egg-info/requires.txt delete mode 100644 unitgrade.egg-info/top_level.txt delete mode 100644 unitgrade/Report_resources_do_not_hand_in.dat delete mode 100644 unitgrade2/__pycache__/__init__.cpython-38.pyc delete mode 100644 unitgrade2/__pycache__/__init__.cpython-39.pyc delete mode 100644 unitgrade2/__pycache__/unitgrade2.cpython-38.pyc delete mode 100644 unitgrade2/__pycache__/unitgrade_helpers2.cpython-38.pyc delete mode 100644 unitgrade2/__pycache__/version.cpython-38.pyc delete mode 100644 unitgrade2/__pycache__/version.cpython-39.pyc delete mode 100644 unitgrade2/unitgrade/ListQuestion.pkl delete mode 100644 unitgrade2/unitgrade2.py delete mode 100644 unitgrade2/version.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..335ea9d --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018 The Python Packaging Authority + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 822df1c..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include *.dat diff --git a/README.md b/README.md index 460eea5..c1a3fc8 100644 --- a/README.md +++ b/README.md @@ -4,47 +4,35 @@ Unitgrade is an automatic report and exam evaluation framework that enables inst Unitgrade is build on pythons `unittest` framework so that the tests can be specified in a familiar syntax and will integrate with any modern IDE. What it offers beyond `unittest` is the ability to collect tests in reports (for automatic evaluation) and an easy and 100% safe mechanism for verifying the students results and creating additional, hidden tests. A powerful cache system allows instructors to automatically create test-answers based on a working solution. - 100% Python `unittest` compatible - - No external configuration files: Just write a `unittest` - - No unnatural limitations: Use any package or framework. If you can `unittest` it, it works. + - No external configuration files, just write a `unittest` + - No unnatural limitations: If you can `unittest` it, it works. - Granular security model: - Students get public `unittests` for easy development of solutions - Students get a tamper-resistant file to create submissions which are uploaded - - Instructors can automatically verify the students solution using a Docker VM and run hidden tests + - Instructors can automatically verify the students solution using Docker VM and by running hidden tests + - Allow export of assignments to Autolab (no `make` file mysteries!) - Tests are quick to run and will integrate with your IDE ## Installation -Unitgrade can be installed through pip using +Unitgrade can be installed using `pip`: ``` -pip install git+https://git@gitlab.compute.dtu.dk/tuhe/unitgrade.git +pip install unitgrade ``` This will install unitgrade in your site-packages directory. If you want to upgrade an old installation of unitgrade: ``` -pip install git+https://git@gitlab.compute.dtu.dk/tuhe/unitgrade.git --upgrade +pip install unitgrade --upgrade ``` If you are using anaconda+virtual environment you can install it as ``` source activate myenv conda install git pip -pip install git+https://git@gitlab.compute.dtu.dk/tuhe/unitgrade.git +pip install unitgrade ``` -Alternatively, simply use git-clone of the sources and add unitgrade to your python path. When you are done, you should be able to import unitgrade: ``` import unitgrade ``` -## Testing installation -I have provided an example project which illustrates all main features in a self-contained manner and which should -work immediately upon installation. The source can be found here: https://lab.compute.dtu.dk/tuhe/unitgrade/-/tree/master/cs101courseware_example -To run the example, first start a python console: -``` -python -``` -Then run the code -``` -from cs101courseware_example import instructions -``` -This will print on-screen instructions for how to use the system tailored to your user-specific installation path. ## Evaluating a report Homework is broken down into **reports**. A report is a collection of questions which are individually scored, and each question may in turn involve multiple tests. Each report is therefore given an overall score based on a weighted average of how many tests are passed. @@ -83,12 +71,12 @@ To register your results, please run the file: >>> cs101report1_grade.py In the same manner as you ran this file. ``` -Once you are happy with the result, run the alternative, not-easy-to-tamper-with script called `cs101report1_grade.py`: +Once you are happy with the result run the script with the `_grade.py`-postfix, in this case `cs101report1_grade.py`: ``` python cs101report1_grade.py ``` -This runs the same tests, and generates a file `Report0_handin_18_of_18.token`. The file name indicates how many points you got. Upload this file to campusnet. +This runs the same tests, and generates a file `Report0_handin_18_of_18.token`. The file name indicates how many points you got. Upload this file to campusnet (and no other). ### Why are there two scripts? The reason why we use a standard test script, and one with the `_grade.py` extension, is because the tests should both be easy to debug, but at the same time we have to prevent accidential changes to the test scripts. Hence, we include two versions of the tests. @@ -97,7 +85,7 @@ The reason why we use a standard test script, and one with the `_grade.py` exten - **My non-grade script and the `_grade.py` script gives different number of points** Since the two scripts should contain the same code, the reason is nearly certainly that you have made an (accidental) change to the test scripts. Please ensure both scripts are up-to-date and if the problem persists, try to get support. - - **Why is there a `*_resources_do_not_hand_in.dat` file? Should I also upload it?** + - **Why is there a `unitgrade` directory with a bunch of pickle files? Should I also upload them?** No. The file contains the pre-computed test results your code is compared against. If you want to load this file manually, the unitgrade package contains helpful functions for doing so. - **I am worried you might think I cheated because I opened the '_grade.py' script/token file** @@ -135,7 +123,7 @@ Yes. That the script `report1_grade.py` is difficult to read is not the principle safety measure. Instead, it ensures there is no accidential tampering. If you muck around with these files and upload the result, we will very likely know. - **I have private data on my computer. Will this be read or uploaded?** -No. The code will look for and upload your solutions, but it will not read/look at other directories in your computer. In the example provided with this code, this means you should expect unitgrade to read/run all files in the `cs101courseware_example`-directory, but **no** other files on your computer (unless some code in this directory load other files). So as long as you keep your private files out of the base courseware directory, you should be fine. +No. The code will look for and upload your solutions, but it will not read/look at other directories in your computer. In the example provided with this code, this means you should expect unitgrade to read/run all files in the `cs101courseware_example`-directory, but **no** other files on your computer. So as long as you keep your private files out of the base courseware directory, you should be fine. - **Does this code install any spyware/etc.? Does it communicate with a website/online service?** No. Unitgrade makes no changes outside the courseware directory and it does not do anything tricky. It reads/runs code and write the `.token` file. diff --git a/cs101courseware_example/instructions.py b/cs101courseware_example/instructions.py index b6e149b..9786bd4 100644 --- a/cs101courseware_example/instructions.py +++ b/cs101courseware_example/instructions.py @@ -18,7 +18,7 @@ for d in os.listdir(wdir): if "__" not in d and d != "instructions.py": print("> ", d) print("") -fprint("The file homework1.py is the file you edit as part of the course; you are welcome to open it and inspect the content, but right now it consists of some simple programming tasks plus instructions.") +fprint("The file looping.py is the file you edit as part of the course; you are welcome to open it and inspect the content, but right now it consists of some simple programming tasks plus instructions.") fprint("The file cs101report1.py contains the actual tests of the program. All the tests are easily readable and the script will work with your debugger if you are using pycharm, however, we will run the script for the command line. ") fprint("To do so, open a console, and change directory to the cs103 main directory using e.g.:") tprint(f'cd "{wdir}"') diff --git a/dist/unitgrade-0.0.2-py3-none-any.whl b/dist/unitgrade-0.0.2-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..88df3c4dfc76af895eddef0e9e1a54c7a0cc5fc6 GIT binary patch literal 17438 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 literal 0 HcmV?d00001 diff --git a/dist/unitgrade-0.0.2.tar.gz b/dist/unitgrade-0.0.2.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..24bc625650b200d9016cc5ada901d05c91e0706f GIT binary patch literal 17257 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 literal 0 HcmV?d00001 diff --git a/dist/unitgrade-0.0.5.tar.gz b/dist/unitgrade-0.0.5.tar.gz deleted file mode 100644 index debd32cec15ab13d6feb708942d2a2a9f1400e1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30274 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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b5a3c46 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.py b/setup.py index 29108c8..aaa1240 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,35 @@ -from setuptools import setup -from unitgrade.version import __version__ -setup( - name='unitgrade', +# Use this guide: +# https://packaging.python.org/tutorials/packaging-projects/ + +# from unitgrade2.version import __version__ +import setuptools +with open("src/unitgrade2/version.py", "r", encoding="utf-8") as fh: + __version__ = fh.read().split(" = ")[1].strip()[1:-1] +# long_description = fh.read() + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setuptools.setup( + name="unitgrade", version=__version__, - packages=['unitgrade', 'cs101courseware_example'], + author="Tue Herlau", + author_email="tuhe@dtu.dk", + description="A student homework/exam evaluation framework build on pythons unittest framework.", + long_description=long_description, + long_description_content_type="text/markdown", url='https://lab.compute.dtu.dk/tuhe/unitgrade', - license='Apache', - author='Tue Herlau', - author_email='tuhe@dtu.dk', - description='A lightweight student evaluation framework build on unittest', - include_package_data=True, - install_requires=['numpy', 'jinja2', 'tabulate', 'sklearn', 'compress_pickle', "pyfiglet"], + project_urls={ + "Bug Tracker": "https://lab.compute.dtu.dk/tuhe/unitgrade/issues", + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + package_dir={"": "src"}, + packages=setuptools.find_packages(where="src"), + python_requires=">=3.8", + license="MIT", + install_requires=['numpy', 'tabulate', 'tqdm', "pyfiglet", "colorama", "coverage"], ) diff --git a/unitgrade2/__init__.py b/src/unitgrade2/__init__.py similarity index 90% rename from unitgrade2/__init__.py rename to src/unitgrade2/__init__.py index 23b1c9d..d62d54d 100644 --- a/unitgrade2/__init__.py +++ b/src/unitgrade2/__init__.py @@ -1,4 +1,3 @@ -from unitgrade2.version import __version__ import os # DONT't import stuff here since install script requires __version__ @@ -34,4 +33,4 @@ def cache_read(file_name): else: return None -from unitgrade2.unitgrade2 import Hidden, myround, mfloor, msum, Capturing, ActiveProgress +from unitgrade2.unitgrade2 import myround, mfloor, msum, Capturing, ActiveProgress diff --git a/src/unitgrade2/__pycache__/__init__.cpython-38.pyc b/src/unitgrade2/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4429c41fb31c60f6c1074d20620caf2572fce98e GIT binary patch literal 1306 zcmWIL<>g{vU|_hl*dWo3m4V?gh=Yuo85kHG7#J9eofsGxQW#Pga~Pr+Qn*r>Q&?IU zqZm_IQ`lM<qL@<HQ#e`}qL@<{QaE#1a#^ETbJ?QUa@nKUb2*|oayg?o85vR-gBdir zUxG~c3rPkU3}S*X8v_FaCj$e6GsvPmkVT9s3^@$7%q5IjObeK67#A|uvVhqvH7pAm zi?vD^7qEg@j0`nQMHVG&DNHqt%}k68CG0hfDa_4G{bIGOHOyHYH4HVZDJ&_hz08aZ z;S8n>feb+m5g<KTC2T2d&5Vo;DGb33n(Tf>j0_A6RRZBfnI)Nd=?ck-$r-77dV05* z%9C!f<W%J*u4IhjPR`FQC`v6Z&dkrNVs**SOI6h5yu}X_jW5Vd&dy1_#gt!si=`m3 zB;yu)N@h`BVs7d!w$zHu;*#Q9thw1KnMJo)3yL!HN^Y^_7o_IhVo52@Ezo4U#g?Cx zm6}{~i!&`VCp8|djlC?jC@H@<wTP900Tjl0w;0n_G873hFfjbGakh#HElw>ejwvb4 zNR4sHPcF?(%_}L6Db33)NiRxFNsTEkN=EREV&da7^D;}~<Mj$EZ*hY?7++qLS&|Ae zyjTPj4U8O&Jd8YyT#PIXT#P)7Qj9DNMIeb}P~d<RfG~&+iaT&z@h~tjlrUs5E?}x* zSO|(<Mn6sFB2aV|u`@6*6mc>zFlaIs@q^ePXM^2dBn(P$JP;>9LPh`-@k}7&nTkXi z7#L7&WMN=n03}kejeUsJ!3a(f%pjYK+Nz97SZWwjn3|dT<!YH~7_(Swm_UiNmnocq z2^#Z@8NwNYp=ltTp|FdEp@c1)tEh>Qp_ZAEp-QfVJ%zP~0g{d&siTCwh8dJfKxrq1 zp_aLhIf5aQp|C50A&&)=azGfCdTuclB^7}}rU>NVDy5L3%+wTxl6-}n{KOQ6M1`cp z6a`2UQ2<4;LSji(u4kG;Nk%G^Ur>;mSX2zsk(8>CSXz>wn^=;WoS2hSsgRzUms*rq zlA5BUP?=w<keryOP*PM0Qks&QlUkChkXfRmkYAFKT2!7{oT`wLU!DiGH7~VXp)9os zlx8(KzzGR0dWt}Cc#8$3{1&HcMRICENoIat5jQC2SRgS6N+Cspph#m(g~T8@F2T{r z1&PL@)Wj4}#K9606AvR3BM+k*qW~ipBL|}lbCDFtK-OFAxs^rvrFkj0*mBcy^7D&s zvE&w)=HB9TPAn)XEy~PGzs2X6T#{Lq8c>v<4$5qrj3Js_5I2L;A~<<~4T}<mCv_t| z1hYsQl-OiIge-`V0}=8d0_<o60d_uzO>TZlX-=vgC|MVCFfcH1uyAp6uyXJN0CxU1 ASO5S3 literal 0 HcmV?d00001 diff --git a/src/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc b/src/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f772f63e4a0c6727d15b104e6cf981814c53043 GIT binary patch literal 22713 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 literal 0 HcmV?d00001 diff --git a/src/unitgrade2/unitgrade2.py b/src/unitgrade2/unitgrade2.py new file mode 100644 index 0000000..a0290ca --- /dev/null +++ b/src/unitgrade2/unitgrade2.py @@ -0,0 +1,705 @@ +""" +git add . && git commit -m "Options" && git push && pip install git+ssh://git@gitlab.compute.dtu.dk/tuhe/unitgrade.git --upgrade +""" +import numpy as np +import sys +import re +import threading +import tqdm +import pickle +import os +from io import StringIO +import io +from unittest.runner import _WritelnDecorator +from typing import Any +import inspect +import textwrap +import colorama +from colorama import Fore +from functools import _make_key, RLock +from collections import namedtuple +import unittest +import time + +_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) + +colorama.init(autoreset=True) # auto resets your settings after every output + +def gprint(s): + print(f"{Fore.GREEN}{s}") + +myround = lambda x: np.round(x) # required. +msum = lambda x: sum(x) +mfloor = lambda x: np.floor(x) + + +def setup_dir_by_class(C, base_dir): + name = C.__class__.__name__ + return base_dir, name + + +class Logger(object): + def __init__(self, buffer): + assert False + self.terminal = sys.stdout + self.log = buffer + + def write(self, message): + self.terminal.write(message) + self.log.write(message) + + def flush(self): + # this flush method is needed for python 3 compatibility. + pass + + +class Capturing(list): + def __init__(self, *args, 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 if self._stdout == None else self._stdout + self._stringio = StringIO() + if self.unmute: + sys.stdout = Logger(self._stringio) + else: + sys.stdout = self._stringio + + if capture_errors: + self._sterr = sys.stderr + sys.sterr = StringIO() # memory hole it + self.capture_errors = capture_errors + return self + + def __exit__(self, *args): + self.extend(self._stringio.getvalue().splitlines()) + del self._stringio # free up some memory + sys.stdout = self._stdout + if self.capture_errors: + sys.sterr = self._sterr + + +class 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 + + +# @classmethod +# class OrderedClassMembers(type): +# def __prepare__(self, name, bases): +# assert False +# return collections.OrderedDict() +# +# def __new__(self, name, bases, classdict): +# ks = list(classdict.keys()) +# for b in bases: +# ks += b.__ordered__ +# classdict['__ordered__'] = [key for key in ks if key not in ('__module__', '__qualname__')] +# return type.__new__(self, name, bases, classdict) + + +class Report: + title = "report title" + version = None + questions = [] + pack_imports = [] + individual_imports = [] + nL = 120 # Maximum line width + + @classmethod + def reset(cls): + for (q, _) in cls.questions: + if hasattr(q, 'reset'): + q.reset() + + @classmethod + def mfile(clc): + return inspect.getfile(clc) + + def _file(self): + return inspect.getfile(type(self)) + + def _import_base_relative(self): + if hasattr(self.pack_imports[0], '__path__'): + root_dir = self.pack_imports[0].__path__._path[0] + else: + root_dir = self.pack_imports[0].__file__ + + root_dir = os.path.dirname(root_dir) + relative_path = os.path.relpath(self._file(), root_dir) + 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) + + def main(self, verbosity=1): + # Run all tests using standard unittest (nothing fancy). + loader = unittest.TestLoader() + for q, _ in self.questions: + start = time.time() # A good proxy for setup time is to + suite = loader.loadTestsFromTestCase(q) + unittest.TextTestRunner(verbosity=verbosity).run(suite) + total = time.time() - start + q.time = total + + def _setup_answers(self, with_coverage=False): + if with_coverage: + for q, _ in self.questions: + q._with_coverage = True + q._report = self + + self.main() # Run all tests in class just to get that out of the way... + report_cache = {} + for q, _ in self.questions: + # print(self.questions) + if hasattr(q, '_save_cache'): + q()._save_cache() + print("q is", q()) + q()._cache_put('time', q.time) # = q.time + report_cache[q.__qualname__] = q._cache2 + else: + report_cache[q.__qualname__] = {'no cache see _setup_answers in unitgrade2.py': True} + if with_coverage: + for q, _ in self.questions: + q._with_coverage = False + return report_cache + + def set_payload(self, payloads, strict=False): + for q, _ in self.questions: + q._cache = payloads[q.__qualname__] + + +def rm_progress_bar(txt): + # More robust version. Apparently length of bar can depend on various factors, so check for order of symbols. + nlines = [] + for l in txt.splitlines(): + pct = l.find("%") + ql = False + if pct > 0: + i = l.find("|", pct + 1) + if i > 0 and l.find("|", i + 1) > 0: + ql = True + if not ql: + nlines.append(l) + return "\n".join(nlines) + + +def extract_numbers(txt): + # txt = rm_progress_bar(txt) + numeric_const_pattern = r'[-+]? (?: (?: \d* \. \d+ ) | (?: \d+ \.? ) )(?: [Ee] [+-]? \d+ ) ?' + rx = re.compile(numeric_const_pattern, re.VERBOSE) + all = rx.findall(txt) + all = [float(a) if ('.' in a or "e" in a) else int(a) for a in all] + if len(all) > 500: + print(txt) + raise Exception("unitgrade.unitgrade.py: Warning, too many numbers!", len(all)) + return all + + +class ActiveProgress(): + def __init__(self, t, start=True, title="my progress bar", show_progress_bar=True, file=None): + if file == None: + file = sys.stdout + self.file = file + self.t = t + self._running = False + self.title = title + self.dt = 0.01 + self.n = int(np.round(self.t / self.dt)) + self.show_progress_bar = show_progress_bar + self.pbar = None + + if start: + self.start() + + def start(self): + self._running = True + if self.show_progress_bar: + self.thread = threading.Thread(target=self.run) + self.thread.start() + self.time_started = time.time() + + def terminate(self): + if not self._running: + raise Exception("Stopping a stopped progress bar. ") + self._running = False + if self.show_progress_bar: + self.thread.join() + if self.pbar is not None: + self.pbar.update(1) + self.pbar.close() + self.pbar = None + + self.file.flush() + return time.time() - self.time_started + + def run(self): + self.pbar = tqdm.tqdm(total=self.n, file=self.file, position=0, leave=False, desc=self.title, ncols=100, + bar_format='{l_bar}{bar}| [{elapsed}<{remaining}]') + + for _ in range(self.n - 1): # Don't terminate completely; leave bar at 99% done until terminate. + if not self._running: + self.pbar.close() + self.pbar = None + break + + time.sleep(self.dt) + self.pbar.update(1) + +def dprint(first, last, nL, extra = "", file=None, dotsym='.', color='white'): + if file == None: + file = sys.stdout + + # ss = self.item_title_print + # state = "PASS" if success else "FAILED" + dot_parts = (dotsym * max(0, nL - len(last) - len(first))) + # if self.show_progress_bar or True: + print(first + dot_parts, end="", file=file) + # else: + # print(dot_parts, end="", file=self.cc.file) + last += extra + # if tsecs >= 0.5: + # state += " (" + str(tsecs) + " seconds)" + print(last, file=file) + + +class UTextResult(unittest.TextTestResult): + nL = 80 + number = -1 # HAcky way to set question number. + show_progress_bar = True + cc = None + + def __init__(self, stream, descriptions, verbosity): + super().__init__(stream, descriptions, verbosity) + self.successes = [] + + def printErrors(self) -> None: + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + def addError(self, test, err): + super(unittest.TextTestResult, self).addFailure(test, err) + self.cc_terminate(success=False) + + def addFailure(self, test, err): + super(unittest.TextTestResult, self).addFailure(test, err) + self.cc_terminate(success=False) + + def addSuccess(self, test: unittest.case.TestCase) -> None: + self.successes.append(test) + self.cc_terminate() + + def cc_terminate(self, success=True): + if self.show_progress_bar or True: + tsecs = np.round(self.cc.terminate(), 2) + self.cc.file.flush() + ss = self.item_title_print + + state = "PASS" if success else "FAILED" + + dot_parts = ('.' * max(0, self.nL - len(state) - len(ss))) + if self.show_progress_bar or True: + print(self.item_title_print + dot_parts, end="", file=self.cc.file) + else: + print(dot_parts, end="", file=self.cc.file) + + if tsecs >= 0.5: + state += " (" + str(tsecs) + " seconds)" + print(state, file=self.cc.file) + + def startTest(self, test): + # j =self.testsRun + self.testsRun += 1 + # item_title = self.getDescription(test) + 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) + self.item_title_print = " * q%i.%i) %s" % (UTextResult.number + 1, self.testsRun, item_title) + estimated_time = 10 + if self.show_progress_bar or True: + self.cc = ActiveProgress(t=estimated_time, title=self.item_title_print, show_progress_bar=self.show_progress_bar, file=sys.stdout) + else: + print(self.item_title_print + ('.' * max(0, self.nL - 4 - len(self.item_title_print))), end="") + + self._test = test + self._stdout = sys.stdout + sys.stdout = io.StringIO() + + def stopTest(self, test): + sys.stdout = self._stdout + super().stopTest(test) + + def _setupStdout(self): + if self._previousTestClass == None: + total_estimated_time = 1 + if hasattr(self.__class__, 'q_title_print'): + q_title_print = self.__class__.q_title_print + else: + q_title_print = "<unnamed test. See unitgrade.py>" + + cc = ActiveProgress(t=total_estimated_time, title=q_title_print, show_progress_bar=self.show_progress_bar) + self.cc = cc + + def _restoreStdout(self): # Used when setting up the test. + if self._previousTestClass is None: + q_time = self.cc.terminate() + q_time = np.round(q_time, 2) + sys.stdout.flush() + if self.show_progress_bar: + print(self.cc.title, end="") + print(" " * max(0, self.nL - len(self.cc.title)) + (" (" + str(q_time) + " seconds)" if q_time >= 0.5 else "")) + + +class UTextTestRunner(unittest.TextTestRunner): + def __init__(self, *args, **kwargs): + stream = io.StringIO() + super().__init__(*args, stream=stream, **kwargs) + + def _makeResult(self): + # stream = self.stream # not you! + stream = sys.stdout + stream = _WritelnDecorator(stream) + return self.resultclass(stream, self.descriptions, self.verbosity) + + +def cache(foo, typed=False): + """ Magic cache wrapper + https://github.com/python/cpython/blob/main/Lib/functools.py + """ + maxsize = None + def wrapper(self, *args, **kwargs): + key = (self.cache_id(), ("@cache", foo.__name__, _make_key(args, kwargs, typed))) + if not self._cache_contains(key): + value = foo(self, *args, **kwargs) + self._cache_put(key, value) + else: + value = self._cache_get(key) + return value + + return wrapper + + +def get_hints(ss): + if ss == None: + return None + try: + ss = textwrap.dedent(ss) + ss = ss.replace('''"""''', "").strip() + hints = ["hints:", ] + j = np.argmax([ss.lower().find(h) for h in hints]) + h = hints[j] + ss = ss[ss.find(h) + len(h) + 1:] + ss = "\n".join([l for l in ss.split("\n") if not l.strip().startswith(":")]) + ss = textwrap.dedent(ss) + ss = ss.strip() + return ss + except Exception as e: + print("bad hints", ss, e) + + +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. Ensures method always produce same result. + _cache2 = None # User-written cache. + _with_coverage = False + _report = None # The report used. This is very, very hacky and should always be None. Don't rely on it! + + def capture(self): + if hasattr(self, '_stdout') and self._stdout is not None: + file = self._stdout + else: + # self._stdout = sys.stdout + # sys._stdout = io.StringIO() + file = sys.stdout + return Capturing2(stdout=file) + + @classmethod + def question_title(cls): + """ Return the question title """ + return cls.__doc__.strip().splitlines()[0].strip() if cls.__doc__ is not 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): + if self._with_coverage: + if not hasattr(self._report, 'covcache'): + self._report.covcache = {} + import coverage + self.cov = coverage.Coverage() + self.cov.start() + self.setUp() + + def _callTearDown(self): + self.tearDown() + if self._with_coverage: + from pathlib import Path + from snipper import snipper + self.cov.stop() + data = self.cov.get_data() + base, _, _ = self._report._import_base_relative() + for file in data.measured_files(): + file = os.path.normpath(file) + root = Path(base) + child = Path(file) + if root in child.parents: + with open(child, 'r') as f: + s = f.read() + lines = s.splitlines() + garb = 'GARBAGE' + + lines2 = snipper.censor_code(lines, keep=True) + assert len(lines) == len(lines2) + + for l in data.contexts_by_lineno(file): + if lines2[l].strip() == garb: + if self.cache_id() not in self._report.covcache: + self._report.covcache[self.cache_id()] = {} + + rel = os.path.relpath(child, root) + cc = self._report.covcache[self.cache_id()] + j = 0 + for j in range(l, -1, -1): + if "def" in lines2[j] or "class" in lines2[j]: + break + from snipper.snipper import gcoms + fun = lines2[j] + comments, _ = gcoms("\n".join(lines2[j:l])) + if rel not in cc: + cc[rel] = {} + cc[rel][fun] = (l, "\n".join(comments)) + self._cache_put((self.cache_id(), 'coverage'), self._report.covcache) + + def shortDescriptionStandard(self): + sd = super().shortDescription() + if sd is None: + sd = self._testMethodName + return sd + + def shortDescription(self): + sd = self.shortDescriptionStandard() + title = self._cache_get((self.cache_id(), 'title'), sd) + return title if title is not 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 is None: + self.__class__._outcome = {} + return self.__class__._outcome + + def _callTestMethod(self, testMethod): + t = time.time() + self._ensure_cache_exists() # Make sure cache is there. + if self._testMethodDoc is not None: + self._cache_put((self.cache_id(), 'title'), self.shortDescriptionStandard()) + + self._cache2[(self.cache_id(), 'assert')] = {} + res = testMethod() + elapsed = time.time() - t + self._get_outcome()[self.cache_id()] = res + self._cache_put((self.cache_id(), "time"), elapsed) + + def cache_id(self): + c = self.__class__.__qualname__ + m = self._testMethodName + return c, m + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._load_cache() + self._assert_cache_index = 0 + + def _ensure_cache_exists(self): + if not hasattr(self.__class__, '_cache') or self.__class__._cache == None: + self.__class__._cache = dict() + if not hasattr(self.__class__, '_cache2') or self.__class__._cache2 == None: + self.__class__._cache2 = dict() + + def _cache_get(self, key, default=None): + self._ensure_cache_exists() + return self.__class__._cache.get(key, default) + + def _cache_put(self, key, value): + self._ensure_cache_exists() + self.__class__._cache2[key] = value + + def _cache_contains(self, key): + self._ensure_cache_exists() + return key in self.__class__._cache + + def wrap_assert(self, assert_fun, first, *args, **kwargs): + # sys.stdout = self._stdout + key = (self.cache_id(), 'assert') + if not self._cache_contains(key): + print("Warning, framework missing", key) + self.__class__._cache[ + key] = {} # A new dict. We manually insert it because we have to use that the dict is mutable. + 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, f"Key {id} not found in cache; framework files missing. Please run deploy()") + + # The order of these calls is important. If the method assert fails, we should still store the correct result in cache. + cache[id] = first + self._cache_put(key, cache) + self._assert_cache_index += 1 + assert_fun(first, _expected, *args, **kwargs) + + def assertEqualC(self, first: Any, msg: Any = ...) -> None: + self.wrap_assert(self.assertEqual, first, msg) + + def _cache_file(self): + return os.path.dirname(inspect.getfile(self.__class__)) + "/unitgrade/" + self.__class__.__name__ + ".pkl" + + def _save_cache(self): + # get the class name (i.e. what to save to). + cfile = self._cache_file() + if not os.path.isdir(os.path.dirname(cfile)): + os.makedirs(os.path.dirname(cfile)) + + if hasattr(self.__class__, '_cache2'): + with open(cfile, 'wb') as f: + pickle.dump(self.__class__._cache2, f) + + # But you can also set cache explicitly. + def _load_cache(self): + if self._cache is not None: # Cache already loaded. We will not load it twice. + return + # raise Exception("Loaded cache which was already set. What is going on?!") + cfile = self._cache_file() + if os.path.exists(cfile): + try: + with open(cfile, 'rb') as f: + data = pickle.load(f) + self.__class__._cache = data + except Exception as e: + print("Bad cache", cfile) + print(e) + else: + print("Warning! data file not found", cfile) + + def _feedErrorsToResult(self, result, errors): + """ Use this to show hints on test failure. """ + if not isinstance(result, UTextResult): + er = [e for e, v in errors if v != None] + + if len(er) > 0: + hints = [] + key = (self.cache_id(), 'coverage') + if self._cache_contains(key): + CC = self._cache_get(key) + for id in CC: + if id == self.cache_id(): + cl, m = id + gprint(f"> An error occured while solving: {cl}.{m}. The files/methods you need to edit are:") # For the test {id} in {file} you should edit:") + for file in CC[id]: + rec = CC[id][file] + gprint(f"> * {file}") + for l in rec: + _, comments = CC[id][file][l] + hint = get_hints(comments) + + if hint != None: + hints.append(hint) + gprint(f"> - {l}") + + er = er[0] + doc = er._testMethodDoc + if doc is not None: + hint = get_hints(er._testMethodDoc) + if hint is not None: + hints = [hint] + hints + if len(hints) > 0: + gprint("> Hints:") + gprint(textwrap.indent("\n".join(hints), "> ")) + + super()._feedErrorsToResult(result, errors) + + def startTestRun(self): + # print("asdfsdaf 11", file=sys.stderr) + super().startTestRun() + # print("asdfsdaf") + + def _callTestMethod(self, method): + # print("asdfsdaf") + super()._callTestMethod(method) + + +def hide(func): + return func + + +def makeRegisteringDecorator(foreignDecorator): + """ + Returns a copy of foreignDecorator, which is identical in every + way(*), except also appends a .decorator property to the callable it + spits out. + """ + + def newDecorator(func): + # Call to newDecorator(method) + # Exactly like old decorator, but output keeps track of what decorated it + R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done + R.decorator = newDecorator # keep track of decorator + # R.original = func # might as well keep track of everything! + return R + + newDecorator.__name__ = foreignDecorator.__name__ + newDecorator.__doc__ = foreignDecorator.__doc__ + return newDecorator + +hide = makeRegisteringDecorator(hide) + +def methodsWithDecorator(cls, decorator): + """ + Returns all methods in CLS with DECORATOR as the + outermost decorator. + + DECORATOR must be a "registering decorator"; one + can make any decorator "registering" via the + makeRegisteringDecorator function. + + import inspect + ls = list(methodsWithDecorator(GeneratorQuestion, deco)) + for f in ls: + print(inspect.getsourcelines(f) ) # How to get all hidden questions. + """ + for maybeDecorated in cls.__dict__.values(): + if hasattr(maybeDecorated, 'decorator'): + if maybeDecorated.decorator == decorator: + print(maybeDecorated) + yield maybeDecorated +# 817 diff --git a/unitgrade2/unitgrade_helpers2.py b/src/unitgrade2/unitgrade_helpers2.py similarity index 84% rename from unitgrade2/unitgrade_helpers2.py rename to src/unitgrade2/unitgrade_helpers2.py index 8421562..d007fd2 100644 --- a/unitgrade2/unitgrade_helpers2.py +++ b/src/unitgrade2/unitgrade_helpers2.py @@ -2,19 +2,13 @@ import numpy as np from tabulate import tabulate from datetime import datetime import pyfiglet -from unitgrade2 import Hidden, myround, msum, mfloor, ActiveProgress -from unitgrade2 import __version__ +from unitgrade2 import msum import unittest -# from unitgrade2.unitgrade2 import MySuite from unitgrade2.unitgrade2 import UTextResult - import inspect import os import argparse -import sys import time -import threading # don't import Thread bc. of minify issue. -import tqdm # don't do from tqdm import tqdm because of minify-issue parser = argparse.ArgumentParser(description='Evaluate your report.', epilog="""Example: To run all tests in a report: @@ -109,31 +103,27 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa show_tol_err=False, big_header=True): - from unitgrade2.version import __version__ + from src.unitgrade2.version import __version__ now = datetime.now() if big_header: ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom") b = "\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] ) else: b = "Unitgrade" - print(b + " v" + __version__) dt_string = now.strftime("%d/%m/%Y %H:%M:%S") - print("Started: " + dt_string) + print(b + " v" + __version__ + ", started: " + dt_string+ "\n") + # print("Started: " + dt_string) s = report.title if hasattr(report, "version") and report.version is not None: s += " version " + report.version - print("Evaluating " + s, "(use --help for options)" if show_help_flag else "") + print(s, "(use --help for options)" if show_help_flag else "") # print(f"Loaded answers from: ", report.computed_answers_file, "\n") table_data = [] - nL = 80 t_start = time.time() score = {} loader = SequentialTestLoader() for n, (q, w) in enumerate(report.questions): - # q = q() - # q_hidden = False - # q_hidden = issubclass(q.__class__, Hidden) if question is not None and n+1 != question: continue suite = loader.loadTestsFromTestCase(q) @@ -143,12 +133,11 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa q.possible = 0 q.obtained = 0 q_ = {} # Gather score in this class. - from unitgrade2.unitgrade2 import UTextTestRunner - # unittest.Te - # q_with_outstanding_init = [item.question for item in q.items if not item.question.has_called_init_] + from src.unitgrade2.unitgrade2 import UTextTestRunner UTextResult.q_title_print = q_title_print # Hacky UTextResult.show_progress_bar = show_progress_bar # Hacky. UTextResult.number = n + UTextResult.nL = report.nL res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite) @@ -157,20 +146,16 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa assert len(res.successes) + len(res.errors) + len(res.failures) == res.testsRun - # possible = int(ws @ possible) - # obtained = int(ws @ obtained) - # obtained = int(myround(int((w * obtained) / possible ))) if possible > 0 else 0 - obtained = int(w * obtained * 1.0 / possible ) if possible > 0 else 0 score[n] = {'w': w, 'possible': w, 'obtained': obtained, 'items': q_, 'title': qtitle} q.obtained = obtained q.possible = possible - s1 = f"*** Question q{n+1}" + s1 = f" * q{n+1}) Total" s2 = f" {q.obtained}/{w}" - print(s1 + ("."* (nL-len(s1)-len(s2) )) + s2 ) + print(s1 + ("."* (report.nL-len(s1)-len(s2) )) + s2 ) print(" ") - table_data.append([f"Question q{n+1}", f"{q.obtained}/{w}"]) + table_data.append([f"q{n+1}) Total", f"{q.obtained}/{w}"]) ws, possible, obtained = upack(score) possible = int( msum(possible) ) @@ -185,10 +170,12 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa 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") +")") + from src.unitgrade2.unitgrade2 import dprint + dprint(first = "Total points at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +")", + last=""+str(report.obtained)+"/"+str(report.possible), nL = report.nL) + + # print(f"Completed at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +"). Total") table_data.append(["Total", ""+str(report.obtained)+"/"+str(report.possible) ]) results = {'total': (obtained, possible), 'details': score} return results, table_data - - diff --git a/src/unitgrade2/version.py b/src/unitgrade2/version.py new file mode 100644 index 0000000..a0235ce --- /dev/null +++ b/src/unitgrade2/version.py @@ -0,0 +1 @@ +__version__ = "0.0.2" \ No newline at end of file diff --git a/unitgrade.egg-info/PKG-INFO b/unitgrade.egg-info/PKG-INFO deleted file mode 100644 index cc1c911..0000000 --- a/unitgrade.egg-info/PKG-INFO +++ /dev/null @@ -1,10 +0,0 @@ -Metadata-Version: 1.0 -Name: unitgrade -Version: 0.0.5 -Summary: A lightweight student evaluation framework build on unittest -Home-page: https://lab.compute.dtu.dk/tuhe/unitgrade -Author: Tue Herlau -Author-email: tuhe@dtu.dk -License: Apache -Description: UNKNOWN -Platform: UNKNOWN diff --git a/unitgrade.egg-info/SOURCES.txt b/unitgrade.egg-info/SOURCES.txt deleted file mode 100644 index 9026e79..0000000 --- a/unitgrade.egg-info/SOURCES.txt +++ /dev/null @@ -1,21 +0,0 @@ -MANIFEST.in -README.md -setup.py -cs101courseware_example/Report0_resources_do_not_hand_in.dat -cs101courseware_example/Report1_resources_do_not_hand_in.dat -cs101courseware_example/Report2_resources_do_not_hand_in.dat -cs101courseware_example/__init__.py -cs101courseware_example/cs101report1.py -cs101courseware_example/cs101report1_grade.py -cs101courseware_example/cs101report2.py -cs101courseware_example/cs101report2_grade.py -cs101courseware_example/homework1.py -cs101courseware_example/instructions.py -unitgrade/__init__.py -unitgrade/unitgrade.py -unitgrade/unitgrade_helpers.py -unitgrade.egg-info/PKG-INFO -unitgrade.egg-info/SOURCES.txt -unitgrade.egg-info/dependency_links.txt -unitgrade.egg-info/requires.txt -unitgrade.egg-info/top_level.txt \ No newline at end of file diff --git a/unitgrade.egg-info/dependency_links.txt b/unitgrade.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/unitgrade.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/unitgrade.egg-info/requires.txt b/unitgrade.egg-info/requires.txt deleted file mode 100644 index b8b3f85..0000000 --- a/unitgrade.egg-info/requires.txt +++ /dev/null @@ -1,4 +0,0 @@ -jinja2 -tabulate -sklearn -compress_pickle diff --git a/unitgrade.egg-info/top_level.txt b/unitgrade.egg-info/top_level.txt deleted file mode 100644 index 5f808ad..0000000 --- a/unitgrade.egg-info/top_level.txt +++ /dev/null @@ -1,2 +0,0 @@ -cs101courseware_example -unitgrade diff --git a/unitgrade/Report_resources_do_not_hand_in.dat b/unitgrade/Report_resources_do_not_hand_in.dat deleted file mode 100644 index 9c15fa98decdca1da9d38c1989086f8c4c1ce960..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64 zcmexsUKJ6=z`*kC+7>q^21Q0O1_p)_{ill=8CV)vYp3WjFfcHDl6clIp?P5j1EVbK S4&8?J^4mVwGJ+&nBBKC!^%DsI diff --git a/unitgrade/version.py b/unitgrade/version.py index a23ef3f..a68927d 100644 --- a/unitgrade/version.py +++ b/unitgrade/version.py @@ -1 +1 @@ -__version__ = "0.1.8" \ No newline at end of file +__version__ = "0.1.0" \ No newline at end of file diff --git a/unitgrade2/__pycache__/__init__.cpython-38.pyc b/unitgrade2/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index ac7b672193da207e651f61b4bee3e1791e27a656..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1373 zcmWIL<>g{vU|>*7S4^y8Wng#=;vi#Y1_lNP1_p-WAO;486owSW9EM!RC`LvQn<<AW ziYbLVg(Zcxg)xdbg)N1>g&~S1g(HQtg&~SHg&~D2hb@;qianPjiX)dZiZhohiYu2p ziaVDliieRQg(;XpljkMKHciG`-0|^csYS(^`FZj2ej&*qdqFZF%*MdLz{$YC;0$s{ z9>^VxDGWIbwag`qSxgI<YZw<Y*0O-vEHx|(8H=?_7#FaDSd0ucOhpzYY$;4NjLl4p z3?=L}j48~`O#NcDtToJ895oCztRUC-GBYxSGng_2G6XS1fb?XQu%)mygMAsypvmr6 z#K^$FP$dvvlv$FQm#&bUn4FQSr>A#|sXXZxOHNg8;!4I_+{yX51x2aFATLz0y5#4j zDr$1x;)jXG7i1=9=cL|Z$}hgfQjl1Zaf>}AvnVeyH}w`<YDH#oN%1Y#-0YOhqFby5 zMVWaew^;HEQuA)Hq?G0sXtLd6%TLNmO)j~`nU<N88V}aSUY1&vlwX`$#LmFLaEmD= z?-pa)N`@j)1_p*-7S2{Np~b01#W5wN8L2TY`N^fZsd**EF{OE#CFw<pDX9pSQA~V% zW?p7Ve7s&k<t=Wo=i<wYGD}iH0beWviULLsMjl2UMlMDc1};V(Mkz)Xh9YqW28Lu% zz<?BhFo+F`H*h@hFfcHbFk~?<V5(tQ$QaC^$>^uaT*Se^z)-{qBDfhC7&IA+1VL<& zlfmvT5(9aK2jT=ssDKm{Gl7g}Dw06Ak%fVQ0hE5hHufP>1|v8%FoSF^YO69TVX0wA zVQOaTm#bx}Va#H!VM<|6Vd-TGXJCTH`eKG~hG1y?hcgs*u`radWpfoZF*4LLGcr`k zm9VF<)-XWQ5F}-ku-7nyQV1yBq%hPn*D*&hL^2e1MKI*Cq%Z_CfG{lO++r$9DgwE+ zh!vEMltPL!Q&SX5@)dIO6H^ot6_OHD6d=h%0TjIoi6vFJo@ojt8L3cyK|yL_Q87qI zQmR5?X-R%=Vo7FlVopw_LV9XmYEfcIYKo3RWqzqba$=rBNl_(8X-aBNYDua>W{HkM zeo01ZQF&%@szOSBc^=f(ywq|9XqMCD04E}}=qcg@#RdyV`7KV@isaOSlFa<PB3@9; zu|Q%Dl=O-~DdZMoDkKKMaS4t_E=V*Mr6#6;_^^b;#KXwM$it|{D8R_Y$iXPXTqFxJ zknI+mM`lV&YThmO+{&W-(!7*gY`JMU`T0e+SaOR?b8m4vCl-{H7G>t8-{NyjF3BuQ z4JgV_2W2=-#t=>3C?R;#HqwK7s|b{CAf5sxR&e?O+jdJ7Nil+1BnL{3@*qM1L@0s? qB@m$uBEVil5Fk(8;;_lhPbtkwwF4#oVo+M-VdP-p;^tuG;0FL^J4#Xj diff --git a/unitgrade2/__pycache__/__init__.cpython-39.pyc b/unitgrade2/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index e61f9803216fe17bf58c02cb47dd2fafcfca4760..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1400 zcmYe~<>g{vU|>*k(@YFyWng#=;vi#Y1_lNP1_p-WAO;486owSW9EM!RC`LvQn<<AW ziYbLVg(Zcxg)xdbg)N1>g&~S1g(HQtg&~SHg&~D2hb@;qianPjiX)dZiZhohiYu2p ziaVDliieRQg(;XpljkMKHciG`-0|^csYS(^`FZj2ej&*qdqFZF%*MdLz`?-4;0$s{ z6UZHmDGWIbwag`qSxgI<YZw<Y*0O-vEHx|(8H=?_7#FaDSd0ucOhpzYY$;4NjLl4p z3?=L}j48~`O#KSAtToJ895oCztRUC-GBYxSGo&!EFt9K*Gcz*e2^caIXBaRrf?*^B z*qpi&wiLEzu+M`TG}--%7#SECsszG|GD|Y^(iM^ulQUBF^z?2ql_%X|$*IasT*(;4 zot&RrP?TB>@>UhAOMYIeq9*4pewb){L1uDxPU<bD{Nh_I1&JjYx7brMi}Dh4Q*W`Q zR%8~J6yIXa%}&WIy2V;hl$lp@izUAxHSZQnN@;F^CfhBx{G_bZ<dR#QX_+~x@nCK2 zWvN9;`NgS4pfHPKO3Ay$n6{FkNSJ|v;g_j?Mt*Lpeo1LYs=iBpa%paAUP-ZjX<lYY zdQoCZDuQLCA0MBYmst`YuUAlciyQ2-`0}F6l2nkX#R8x>VdP-sVdP=tV&r4wVbo(R z5@lduNCt%nND_oWY)~YEBaMfFfuV#Ui*W%{4Z}jlU<OS_KTYN$b_NE9B2dH@fzqHR zV-d(?uv;O72m=GdEgp#Vkl+vmr2r<7@k~Wx=r*!2Fff1&0^2wTkxUrDNrM?=bJ3J4 zyAqZfh7=}nB4Dav%wnx!N?}f6>17IMU_wd);S9kHh784S$cZ4Fp>PHZLkU~u1V)Bh zW=4i8wG#Fe)*1##(t;$8680KqP$CIuh-64%sAaBWE}Q|O4Hyg<@>o(Bf*C*<mVj<C z6(tpcTwcTqN=`~4MVYB73MKgpIr)hx3W*9yi75(@RH6WiWrf6&s$9=Bg_4X^D8HZ} zHL<7|q$4R+A+fY1KR2-?GdVFQr&1w3H7~U&u_QG`N1-ymR3SMrPobo!5~MUGH7B(s zRUxxPM<Kr?BekeJvp7{DCBHlmYHMC<xdJpdYI1<n5?W*x@q*%p1*H5Ir)x!WYC%b6 zeqIqMo;6t@@d!#QMM5BDjH!_L1f{|vaHMiUBDE+rF$ENj#UMcjMh+$(MkYocMk7W( zMlMDUMg`^~X$A%cP)hg6Oi4-2yTzVcS(IOzmvW0OH!UYWzvvcAZgFYuEl%gef|Amr z%)Inle2&Q_nPsT~MfvHVe5c76qRAU21W)8fdQfi_fl?8~Q=p^^PEBCjZiyl(Mlg$H zK#5frM96^%c@UuhA{0Rc*lP#^<eys{Ho5sJr8%i~psY{~N|`*294uVi9IPDt05~s6 A&Hw-a diff --git a/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc b/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc deleted file mode 100644 index 3492c9eca22889350f2a5707f0579dc18533a9c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29204 zcmWIL<>g{vU|@*7t&@1Wo`K;ph=YvT7#J8F7#J9ey_gsnQW&BbQW#U1au}l+!8B78 zGni(HVo704Va{RAWs72Cgs5SUVh77{L~(#=&L~bW%@xH3rn#fI!8A`4518hS;sw)u zQG6*ZDXc9FQT!=vU|JxB9ZU<RaHMdyFhmKtGo)~(aJMj|a5pnW3A;0-@TBmzFr@IN ziZnAviMlhS@TKs#Fr@IOiZwGwiMunT2&4$MFr)~kiZ?SyNw_nl2&D+OFr)~lGG<9O zGe=3eGo*;5h_*1Kh^9(4Ge=3gGo*;6h_^7Lh^NXlGe^m~Go(nQNVYJfNT$lBNcA#D z$+<J6NT<lOFr>&dGeya#$fn4(FhnV&$fqc@FhnW3Go&b{D77%8C^a)hDWx!`DCa2W zszj+UGNh=asJ1Xfsivr<sJAdgsik<OXr^ejFh;4TXs773FhpsjFsA6{Xy$4~X+gtL zJ4zcIE;>;<DS9dTEeuh*DU2xwIqbQ5QF@FF?hGl0DMl>}DMqP`S^CY)Q3ffDDaJX5 zxkgb&U=!f6;?9s_l49DzkYbw3m}T6|9A%PXmSWz*5M`Rem|~G*mTMkmo@)_hk!u-c z3D#+u!=7suWyQ#lYL#M@Vx3}>Vw+-@&eF^jWu4-k;*jFl!Wd=a&XD4i;@rZJ;tUN< z+Z2}+*A|8-yA-z+_ZEgI`&9c>yJp5HyA+QU&lZL#hZLq@22G!;7OwQn5{1N+6a_s6 zH8lkgCpkYiH?u@RH&;Q)zn~;DKd)E`CRI>coB?7h6l4}CWabr@B<AFR^k^3sXISa$ zr)QQpq-U1oBqr%4=jRrbmZa*Xl$7eFWb2odW~Azu=4F<o7bT{o>Vd4$)h#Um^SHQP zg3`L*O2%6p!6ikRdFh`1$&4T&C<d{a85kIxK{)`FQA!vVFxD_EWDI6l$>^uabc-pk z;1+99eraCHO2%7^6)PEvI2jliepxwN#e^2878S>UtdDWYPcF?(%_}L6fjb1jGD2eN z6;$5hu*pfxO-f0$6J}sw_zZGL5jO(^1BSbpVD92zU|=X=s9|Vk3}z@|VPIg;WW2>( zT$)?N&cFb157<#fAoqif5&?<xU^5ekUm>P46|pifFhsGY<>coV;WS(nZaBz+Am@WH z$Y2m39FPJG3=B04Sqxc>wM;dPHB4ztFo!eU;*5_^&PgmTj*q{^5g(tIn420OugQFi z(K(7EDX};;J|(m077Iui;wMmOgFSXjs5rHxv;ZU@pHvAkLmU)qpa5ZD<X|Zh0(ltZ z7Gw-^6S#N-g-I$y6k`fQ6cZ@BwJ=06r!WUIXtLa5^T<p|NzKEE08rup`v+u23Mff1 z`GtU;wUY4`OL1yW8khqiK!JaYB_lH>RUYIjki86yRooDx^+4i&nk+^93=9mnxZ>k; z^HWN5Qsd)q@x;d$mL}$aJRBci1o9BXIIzFL1jyhbP*RbDnajZl<|iZj6T}AD3HGZB z0|Nu5UsG6G7@}BGSX0<q7@}BH*n=4~Ic~A}<fo^n7A1r9g3JJ65F3QSeg}nW2}2fR z4MRLAE*avPQy5^e$#RRixU%>bTX9KBerd@qj*`@(+|0bhoLkH}`RSTWMPeY|vL%(K zrKJ`jc?;wtP~>OkWtPOpD}%fR3SkCD4#p~eh<o(n;~}yb4g)1Lu)}1aiEII54dX&a zMo@xdg0T@!WG<2gIfAvkD6=HB2o!`ccd_TD78fU`r=mHmNRfeoK^@^H9>yvjsGGoM z5J)Akuz{Eg&MTat3<D;>9%oI<DJ{;>2H5}#WCkX<`Cth@O*U{UDUtwblLQe8AOhs9 zB2ZXCOa_|=CP0Q3Nii@mD1nRwRp%Ux94s6xU@?p|0uE@9LNl~9lEOTPshuH>5nMHM zMKPyvr?97Rv@k}oq;RHifl^F60}DeGTQGwr&n-^p#DbDiP<iU-rpZ(Uii%rorFprf zC8_Z60eKEoo`50@l<bQ^fnNhkM+`MgH4O2LB}~l>S<E%eH7tFMj0}h<W4Xm14@pr) zs^GXQE-gqcDguRqCM!5ifh~kcvLqIz7vEybE(g&o8H@NpVTA}6kR}6=cR}I8z{tkP z##ki;cRW0mg<vEzkn6$8EDD;;QW&zCiUdj+vzYQ&KxJDELp(zXb2CE?Lp+GjT4Ykf zR>P3R(#%-G7|)c)0b+w#HH<|8C5&0@H4O0_AoV4T@tojL;HrV86;19UHITEVK?Eo) zi$Gy^i!;6$TxDhEgS`rlPjFsKEh@Uj7GGQfqFEsnUotpCQsYyLit>w!p=rEGA7mmp zG{6KXselUL)Vz|^qWE}IP{4r7MFvI*Mh-?cMm{DECKg5xFuO_^Ihde2;E@X}CP2x_ z9b}9VIG7fIav|dariBa(m=`iYa%BP|I4D@-LBYdbq*ud`#Zkf<&xwd|u3K!W6(y;8 zDMg@Maf>58wWKUDr!@5zS8+j3W=T$FUTQIV2!Va33-X>Gs6vF4HANaA0kBuW1lYeE z@$sn@pn?V*ogg6wMiEAkH__7qM42XIh$cJMEC@<SMJAvm$qTK-l2dbX;^UEWq!!2l zAUldcWiZH*#h{eO!6?GY#RnF{$dRCw1}?QhF@afXgKH=*cw88vL;);+fukS<90gen z3m6wNfTEz5DTQ$Xa}Co%##-hQmKx?}##)vdh6Su35k`g@h6GS#v6ZmLgVHKzkxLCj z7FP*-Ja-Lq4MRLn4NDC}JTEAcG@1NvF>-10fn%o#6wyV-pg>^B%FoQZ#h+SHQk0ln z5}#L^o0M8qTx19e0<`oD3Wy?jY~5nZFD)r3ExE-G)dS1cpy<2B3Xbnv%q0~iMb;p* zIN=01U_lY!4N7vL@M2)(W8`AwVB~^EC<hZ8BT~vW(t|}ltnx)FRzU_KMIG20unnN1 z(F<e)C~7zui$HvgXahwnC>In@K`BT<{iP_D6s9QF6y_+l6h=@VDT*V7C5kge23)*? zs$b41E^y(>5XGG$3oc$6qj*x}Qn*uiS{S2vQ{+>4Q}|jKqxe$zQv|^MB*7G+7KSK+ z6a{cUhA~PI(tn8(N>K#&To|K-Q>0R)TNt85z{Rv^FoUMjE!IHKlGI$kB2Yu-`NJxa zb?y&u*}nwwP|7<{CI*!_;8HpTnu<~wvssGdY8bK@Qy8*2z$|d7ohJkrGpGTT-wg5K z44%bW!w}B~F4@>iIGP!<IBS?{m_a2wC|!ZgNT^}RVyj_@=K`gwl}r$KX>#9UF3Hci z#a@zHT#{CrcS|6#xHz?_#K$NoH7Btovn=%%H$=o0)KG#H@F15K34#(KM`3AdF{md| z1gf*amBcO9lFX8vR81C0UEu~wKA@CUqzPhyb0{LNK@~(2D6T>698kf)1xim$Tuf|C zpp;d`2M$7bMS+t0KmiF#BH)4r)Ybvj3n>iQoJ9g4Rxl`k!JF(%Q9|(rMXAuvOG-T0 zjYXi0TZFBZz#m_nn3fu!o>~G?Spae)IL$F~fhsXksJl>AC4-_8lqx|Oq#A_5&g%n3 zD>Jwf;{!Rambrv+0aFR6fM8t6n8gBSF)d`2V8~)w$XE;Fvz9O}V5?zCVH9Rq$heTP zmbC^H#q2e#HO$#8MO`HvDNHrYvl&vDYglJ9%w=h2WMnAeNMWgAS-`oFVK&2DCXfgy z@@p7tm>_EBGNrKgGD9kRO*X%mpwRmz?U!GokP3=ih4RdjjLbZRlKh<1qQt!9RD~)@ zYlXc0qFjaiG=-E*P&+aeBwU_Ytnf?HH76C+>C!FFEKWs|Eml~`c#Ao=I9-#w$O#nX zpi=S{b7E5QE%wCX#G<0a$|#n!oWznSmg2&qk|JJEI^qLYobj;exy4#gl$lp@i>)L- zC$qSu$Q4w{uz?fQEe^OMj?{{R)Z~)X6tF$FSU@g}V$LhdMJiT6CCV)>NDB7LFUl<i zMI<D#fvQ|NMiC}9CJsgsMj^&30Z61G6r;o^$Qz)93yx1vsZ)b9J`u4AjZ2mqmW80E z7bs34Y)~9dC}B%stYMzblmbc#VDZ^Z(3oUPVXk3Wz>X4=9N?IQ=m4b<xDJ$9t&+zR zp(Xh_wpFs$3Mr{&nTeo$s{o42yaK)4#0m{i_-Wb}flBu(ejNo=Q6EqgaDqd+2ow}W zexSJZ2SpxpZem4I0Ein1B7#5!D8^Pon~T990Z{b_(H#onf<h7!P2d>ffCRdaQ4=VB zKpj|6cZ7{mijjwjk5LK~JJ6y4rVt};K;3k3+^oQx*HGgIl)sR3B1~4CVIiY9gERv; zCxXJ72^53yIAp6~0p-Glj44dnEJaI7*i)E6c@dK5O4z}<kOL*|K#fi&m@1U`O1J7U zc+^+vY6q?-ia<3VI0}luA<hK}Gt>|-3I_!tIK)A@rwBFJiy}a>r~zE$!oa}L3kq9A z+LvNfViaN&V&nsbt{8IKM-=`T1um%21&1=IXakjGu+AvsEfJIgwji-GCqFR-Yw0SC zToOYS&Hy<MR3I@hR!Kq}gGCi?_kfCCP{jrwOM#>;Yz4az!q9lA&9mV)GgV2T+YD3W zhEipN+zG<qkN}PJlrW?)HZwLcf?S}<<X6Q9s!kP>^HWk4GE$3DH5qRS`{kE-<`zK8 z%+wTDP)7mQPXP6^AWjCy8aPMrL5sQg#JuA2)S~%tM>1B4KpcsxAQ{wz0M$Dn3}S-< z1nfRgZmMBezyK;e7BWFfe{d*iG8TapXflHf)*?hU1-k-FfStku^2{P62XH|g01{3H z4T6EJ1YuCS0Hgy{f)rOUGBBht)^emU)N-aUr7*WJ)N+A)w+yx1poCM)QzcWwQlwMD zn8F6Fs+ek6QrLSLYdOOi@~T)EN|>{`incH^)bcViRK?V=7B8t`E1puqn8E>4U&5He z3F_L+W=P?h%Ur|T%*4o0!(GDzl1t%EVaQ>v<tt&V;akAAkO3rC!k7Y;tKmpvVq*wr zh-6sA7R(UNV9JofP|I7#8^I9CkjE4OYB#WhTTI7lIBOW<*=v}J&($y&T`OTs;RTsg z!&LMLW>XDw(YqS18cvXSl|&8e1jb@ef0Uz!vxX;yua}9DA)FyE1LBtnj751+-+0up zO<*hzLh=hg)Gq>anL%ciFxEiqf|?DpFNGV)^-RHNE>B?u$p$lM3i=g+IyD&C1(aw( z@mvgQy+aZ$BXn>Rk}yDtSHRBBP9ZYCv`7KcQLtLc2rgnY86jx|oHD=!D6((y6=&p^ z!>jIiQ1M#~N_?Oayh;O-R6q@>g8a<9l43oZoc!d(oMJmYRBhxr)eYoSS7<W;>Rbhl zy!?_>D}~&|N`;KXvQ&kn)YLqMf};H7)Z*gQ6l;Z|)Wj5plGKV4h2rF*%z_e4tD*!@ z?Z#LHuB5;|0~1KzgEckc*MRZ?(cXh=yT!1Q5j>7i6~tvrZBQj2o{^fTP*PNxnU}6m zlCMxynx_EDoeJQwUIq0kX?2B4P=ta@q>_xH)N+NyJOyy0%L-iYR0UGoQMN_qpg?7U zq#01iR>_8>7MH+cDXl0!7uAuv>Iy0Osl^I;`6UXui6zMy3b5c)C`v6Z%_-4S02=^t zwkBtkczkkxQBi7g3AB)e71rR?S`-Irn{c{TB&UMsIv^buPEc<-H7PMUJBlkUzbH4c zBtEqw`IZ18{X)vyqIi(w`N1XQE%vh1qNM!d)F@tXnuWOv%7vE0Dd6ahVlPN6E>6tJ ziDE0x%t_5F0e5?%I12KMi!+mQQX!#{2&$n${hlaxs1I&2rWU1vM8OFUOn_4#7b52@ z1*JSttBZk=1=QbW;b387WCF1m1sDY&VvHi-@gp8a4n`#=E=Ew(mIJH;G~mL-$Of+P z*%&z(g}~}K80Fy2UW7#`SrJrXfLsEqDT=Ka7#ON}K$Xct25`xy!NO3&n9WsW!pKm| z$jDH|1uj)tK%@0347H4PjIc5lJSoKt>2rdMCQT-A5ErF_!WlY#l!+w(Sc{YMi&C$F z{0?#k10xe752FC11Y;F9q-X)lYcl#FAvaAnH!#o?Lmx>3_xM150k^Ae@qv1;xrrsI zpdMjvY7uC-u_zrh4Z~KRnwg$a0`Afld4L=i1|lLs1gJ4p6b)j<fCx}RgLozj#LWf~ zpus()DjnSI1-ZY-j)8#zWJob+gbEZlptyu$CO$q6RxVyHZZ2*PW)2oEW)5a9L$D5v z@efc94=Uk{K|?*5!&cDA2F?_=6!sRzDAp8?U<OUDTU>zwpner-SOO)IL9IGaM1umR z7^OV}noDF(WM^Vv;8K7B_td=9qQsI^SVk_-$V|>q$SejGJV~kGES;L7r{Gknkdm5~ zSejF!qmYzek^w1kL4)`upt34op(F#O7&2v{P+ANsy1>0Y4Lv<QO@+i_h4R##91vZU zT2fk+rvM(5EQUKHu_#rcI5jT?WG%FCkB8J(3L3?!sS40+2`w5Fl2UW>%Qf{BJo6Ot zOEOZ66w339Qi^q;uBa$cNQZhWMIp1;iVJQ{QEIV*E!;3iaAPbU;!aJtqLPXd1zQD; z#Joxc*dz=rbP&cSCgqo<BJ@B9-(dP++%(W+rvj)K4xP=>g?g%_qC^wnEYS3q9;k@d zK=r#uQ7Xt&6(u?fP!lzE6l`E67PK6-Lo&z;61r9jkQw2`9B4Qzq~#ZZy`5N;S)88- z_iB|Rdi;YLxOxg98Sn&=S*!pm1XICL19m8?8~rpH!Py>b-#`R4-XKAJ50w2u36z1i zN*<AhVRO;YIEJW2=@^14YEa?^Cu|U>1~wuD&YaLes$0xOsl`RMpnQvE$nzE-s`KB% z9RupAz%!H{sw&hD1E{wID%8R50CkW{7-|?(7<(D}`D&SJz+=X>%%GNNAVUyC1Vakb z0w&NnaV<*;a}5h<<hX<dOtXUKYid~2Kx4$LH4F>bApC`(nN()KBGBj|q}pW%k9HU3 zf&63vstEaua^nk%^3#h_i;Lrv5{rr~K~kWit_U=^r3o2b0oP$gpklr#AEb-180#P~ zsKomK3I@=84+A3)qY!A=j*)|r2Q^4g<I+!)(G7EY4eUga6TlTVI9iYhaG9CQz`$@9 zWF06NIT$(kIhcz;Vz^61(6}P3R7?f0p=n`g22G|hgG)x%6xLt{O}1OY{zWONMX4#y zpeb?RRPYEoMn?*i?!fT|8esq}P*@0`IbyoSot&SOlbQ?~N-4g@4K>asGr0uTs{#*` zvV!K;i_yGwi#s0FV=hQ6N{x^I1@a!qCI+S|d8jeSuF^wNi;}`YK?1^H7lOvyN*HRG zL30qGR8zy`!Vt?>%UZ)y!<xlp&QQxzC;*v#uVHavXlAToPGPoTC}Cc}QUmTD)-W$* z0nK>$q0|l_XMyq#xZ?nt7folVVTcu|Wvl@WPO}%W)G$tDDg<d{)MP>$;Rhvn&>9Xs zgImnmsg>y7=duBf?SYbr9U~(H19+x}je&`QgP}?vdkEyEmdD2<IuHo`prFi$L}h%u zChIL0kjb}LvQsOIi>g8Cn5Cq$AoUh|d_33)SYijoO;H)Bw&w&llv6U3OQM*vi*GR| zp*gmwo`HekFDQ;c2@;gzL1|rxu}T_`OEKq?Z*ha|$xSWE$WJLM2jxJJ!AO>Y!VJ9N zqNtdGf#D~}Mo{c>FfuW6F@eQ!C;A-psSu_pmK5eF))bZ~wiKo)_7v7Ajuh4?&QvaN zvjCJKxKp{acv3i0*umL@JCzGO9RiZ!P36tvN#O>~JvW0UMZinIqWB<7zoPiT8A#w3 zZy<F1-@Pcmw4h2bG!Hzmo}vJo7jOgjE{g0K7#L6!6{rCN@)?LO1`TC(Fk~^LFk~|o zd8IRSFvNowxwSBWCvBKhm|8eG7_t~aX%>V*Nza8LR;-o@qzhD5b}*!{^nw>66dHlV zf*CYfQ7r_eV$d)eI2D5$U*J?MQ_Bd-(x5dAMJy#uHH;<9%}j_C4UQR2mRszZdBvcy z<|y{eVsI4R;>s*8E=>Zn!7a3vOhpx-l*W`^0@ef}K&hpuih+Sa5R}3}1qdjGb1`r+ zRH>r`onB^1YA#ae7?HLyvLeV6!k~l!n$ZV)LJSlhDGb@{MJy!@9Sk*$%}huc65JF8 z6^NRQx7gEDOA<>;ii%o6X%8_#hU!jGynx)v!Jx@l1e(CnWWL1~4|X9WJFz7e6r|>* zK+Cu&_FPEOd5h5#;%%_OATw{Vf_3qNd=F|@GJrZIjB<>i5%(%#EFpk<m<?ncczz3M ztpa0F2Ll7cN=WzN7Av@g2yp=-A9XV@Fo=Qd2PFUoCY0a=H4Q+*;KCEMhyye*1x|z_ zplP-&(3Bf!I0#<gGv8tbFPcJ{2Lt;RY#o>YITPH~lmgia3Q3TkxfrX2P<;zpazv_U zL9<BEF~eKTWr;cHUImqB3LtAie!}8akaxje1(oKYp-S*DAjA|b-nhjAS_GtmFc94j zAZe8F1lbG9&|phIeI8JFx-i7bff8sIV-`~}TMc6tBWQI3XayvQ9nVzA26hT)L`svT z2ozo5G<QoNBe6I>IWZ?EH3hO7sb~VCuXu|El&~O9hll$uagdAR6LWIFi8UTp2o-C9 z`~b>*4B&PO7h{zwYHZ-przwD~{}?4ukXTfbnV6GP84vD#rbMxofSp(bDuRn9g7Ow9 zuE44D7Dqu*enDzcNhR2Cx7dnPOF(N!K#>LZQPE_Odax%D1UMOiCPP?2P6w?mLhmaw zF>-ORFmf?7F><kjXbzSlka|tVTb#M6C5hmY33pvlgr%-XXNY1=XNY1;XNY1?VUFTR z<%EpaHZw+Xr9!%rpqhajx=fe{vPd|JH$@CSVJ;3{3d|VA4_N{nC6FQkpE3t^B_(gM z1*I0`7nM};6oF|4aDCzT5>x}ABqdNX0wq&$QUY~yp-D+9hN+gZmZ=2P_+V^iDq^bv zFL+^OfFvW(Y6;e&)Z)|<P3Bvi@C0y+JtMIgT=2~Vr6J~9%*i>$w-^g=F(NM?0S7ur zfjKD9A$b=(4b1~_v>w=C6la366v(MyXM*&DYZLGaR&eIhWGZR~RSTekH!U+KRg*D_ zIXNd8J>jzEf+TH0)`Id7XjK~2O0WcmmGF=OHNZhumN0=9sDc*GLab#5TMM2xD5?Xc zFKqdnH6CQEBivTTDws#X5-7HUYy{OjV2^@^LO}76!YIiA%GMydma&96i)8^Ts49iB z*=iU-Q<yc33z=$}z_J`QOeqX0Os!0k3=0_-aF#G<ae>yyfTkvDnA4!W7*LqL{QLj^ ze@*UNJOzo#+3}gVpfpnqTD4e^SdtMRe~UF9#JI(jUwn%N%wkW;ECQ9Ox7dqPb3h_R z3qj$|0h+!Bac{ALS2NvWE>10gcKf0@it_VIKucP0@fJZ>J3x$Ohb$1qXt9ezT@nv! z&&R{`xPSr>6!#2_Lg2oT3}clHH2Sb=a6{>1fYKQVgF_ZHpj5(;1zH>cBAII#7O;Sp za)CmrgtdmDhPjz322?G9R&ua|7IV2U#0tbP)w0yG)__)hu*7o~8h}DMo3khaJpIoE z?rB0Nel(f<G#Nq5*or`X6z-yBptP|Z6jJPoNyVUmDgq7Qg9EP!G*=kKQl64o1g_DF zW`Wc(<@wy=E>10h4YtA>(;GnTXm;qFT9jaUeo=O2UOFgrQ<F>biz<uef~=YcBG8IC zP}{^C6o%jy0;2?z5NPoS6KFXqEEK_|YkWL-<PfFS1O+=NX@Ua}G@_cqn8J|5P|H-q z1nLCUGS@I<fjU8MkRSrfGS{-zuz~8JTJ{=H*9WxXf}@5hi?xO^g{c>`>VgBjb`Gqj zhFyZ8ma~Slh9#c4Pz+R4X);5GF+n9QWMDY6q*9ap76)kYIcP{GiVHMM?USFFl3D}_ zGnSIf+|(#h(1a*Rs@Sb4KNrMsPApEn#TSxVQ3B!xmFDH87TsblD$Ud60*^cwZ2<+> zR#33Bfi$NUA%)^LkT7d;Nn%k+6l-y5W=ZNT){^`ZjAp?tmfXb5ym(LmgCdlH3ADnH z4?O<M1x~P4+|XbKsl&+h@X!S%P1w+HCPOV~*%6})L##^-Q!P_1a|u%oQwpd+_NZZM z2CqM4WT;`vVgZF~3X=_p%?irqS!^~8g+eLJDJ+?c2tH_7Mw8X=7I%DcVp(c@a$<5u zY7xkTRXTb33Sh1RXizU6vI-qCMqdmXCIba}I%w;GkzPS1XzAb{P>_LAY7uB+w`ebj z3o_{z8`N0PqFz`D0Ih|fGPqLcEk0Cxv*BUM2&$DqRRk9!4<jFw0ArOXG%Qhdpk#PZ zF$%9UK#3nzl)5m)io`J0GS{-yFxD_<F-kJjFvHtMpg7QE2B)PWkn@Z7fod^Ua2ko? zfaba4B2X4YN+4+2Y%>D`LpjLhAk#oA_o{@TZbi<`Om3jzIndBDXtYKMTl)t*A5xr| zp9gW!Vo=5x%FIj2EXzzOP0WE7dBsJWKtbOKB0zO&(E<<)R45m%2eCjNF9Ju_eh?Sr zI;2_=>~>J*E}G83z@QIuHb?^~xS@mPj4Vu`rVkS%XqcRXolAs^hl2;K8YKyXN>&gC z7Z9LfrgU)9UBIxA!G$3<0#Z<<fEHLWr!dvBfJQ{JS&G6y#YYN=$12XSkO{Q-Dh0G; z2PV#%0v3merLfhq)UXuufLbD;Q9DM4LY@@%1<atD5VXqQ?-!#gq#yi?u?Ad(g+K=B zSfDF8(=zi?iaJ4|02->(WGey<MMkmZL6%}N=0q_UfCeTCb8azaB83q+cnTR97^*?R z16l#cz{JAH16_?Nz$gQr6XIbk0;xd>6i^BPVQ`>;nqDc4uzqj}LlzULx~^ebzzo`n zQ_BpQysu#fcf6rXSyNcDS&Do>6%u%|nk|JDvc$ESsfn=$RE@LwRXIiLYRB3uXxLkU zaZHMqLX4h5Op3OGra}!=NLwLB&t5@OK@+4Z+BG#+AzE8E)*h<L9>vk1h7rio;10YF z0|NuN@2gVFn8HxQn9Wu!k-`X~ixfa(tIdpgY@jw`Gi+#z=@z425om%*lj#;{i%nw5 zE#}O;5>05goiPzTU_rh1K2X4d>;(0OtCZ2!+F~1t%KXB}z))obFO~EVtb$4_h493p zyv)3G9fgwoe1%-ld;xTnUQv_%7E@8`Eq2g&SY}RY6nmI!kduF~YZN;ubtLA1c6FrY zL1s3=b7m}%R3e&JnwwgbnH-;-pH~d3ltG<~yeOukid)QyIT&3;(5P=G$XB3MA)w>} zD!lj@K@*02jNmb0+<gYnQg7IxY6=gi2ybDGVutQ$VSz5GWrd6&MX{xD2Qz5$-r{pi z2CX6ufDS!}{Nm581Wm?+2ci{{5{r^S#UW_*2nd7NAPi2Jppge?j~}#njuE^v8nl#= zA)dK}rG#|>Xh}P$d(JG)u#mACq=p^5ZknTpsmQ5@0kjYjx~hTM&+YlFnIN#!UX$w< zW63R!_@dH0P+Wi)f!<<DDJePu%7Gx;i$Dd&EylcCg5ddHlwtE-h?2MHB1i*B0BlCl zRS*|kaDfSM_5>ALQ{mwa-T}eJ$i^ta$i~dVSS5}cIJnoj!mA6=s_7JvPeJ}>EK)0B z$^tcUSU?Sz8isgK8;}*W;)Ri+gdN=WW{Bs2Hd;bJ4Zo7aqV&`fP0pfAAjg7Se2Wvb z9w0F#GcP@gEhGc9IHG6=sA<6l5ibIJ5L|=ufSNJ!;1)+J)|FjgwF^Ps1$iE_;X{Ct zi?K=s&7)v5P;xh@(GT|}s2dHc6pQ&v7@-TRKy6--pP7nOK<l1Ctzt$7Pyv(z8n6oi zt!)Oc{ROiYu&1zqn{W(SYzsI*bJZ!Jog!={oLO88xIz39o&`J$LFVyFFo4Gjs}zGv z@(T(;vyh1j#UN&C3Q9WFQ*gV*QUIDc0auQEkPrlAMM%Wo00k(h{D!nUZh?cav>+uB z)GkZT$uCYVLhLY61}O%GY0*s(3tQ)m6FQSvlDYyEpy1-25j1rVN<UnTBH)6ajfsn~ zN&+o#;Tll_7*u?M(j_=+fhMX;7_vYCzz9l0ph*GH0whqTPGRi@ZI@vMZ>V8RVUu8J z2DS6dz%4%ZVxJm@ELNmIWo2Y2Oe^7Bz*WPL#SIN~)&)E%Y#?@_9ysx_Lt4ykFF_Mj zRjSoFpc$yz>ZHV?+8TxE>eQUXg5uPaTAS*kR8Si<GcUb1R+GJGHz*8QKz&s30*Xw~ zEK3w?PHJLVY7|RKYH@NDYhH4GPH_}hQesg&WH|@8e%0i?#Zpq3l3R2I6uO9D2haY4 zV}`XTF)uw85{Gv{ia?QZi?ui>HMOAVGDzSyD5FD07K^a;T0nVZ8z^Q#16K@;VvJ1x zIhchQ*}x0LS(tbjtAx<vrKmK|Pm?*M=sW`h1E{46S}un)T>>g9z!|6rG}i^rgGhA} zDC!^+1LqhR7^Z+M1`TL&Fmmy5uygPgfkZVKZ*j+G7U$)cfV%@2(GM!tz|jvHIZk0* zz)->nt=t*1m_Qw(X3%aGMsQ=C#qSrRE{X}D916nV`WIvrD2GCqf39SNG!#Lr0g)UH zDh0q>V$U!#Fcg8hcU7w3&Bb}iphkX94yZ6m&PJ|CA$xOIvJ`=`(k<rP!~*bG>n-No zqI^xJTO2U$MMa=U0*4Bi09kcQ7$yrIH-s2|8WdEZI-G$~im~VwXqPI;7oge(ghAN? zUhaT~EJ33wpibjLP@<OrCwk@@#w?a>wj!PyMkWSEhG0<p4n8Ez3La^RVoOgg$t}u{ z;>gT}?2ZKo2x!CtG^-RJugMg}0-E<JnuEpNvY^E`;1Rp{l8nTX_>|PN%)Hc;c#wwt zl=C1TfFhfLQ2?Cli{60D@I?0tDDXiZ1xGPxg*wP93mIw|K^2BLLoE|%exn(@8$g^P zo1sVrv;h~?2a{v~58yDTgVsAUEMTr-hU_;e@~dH40Gc{v1?6Q>b2y7Fo2@7ZEXth4 zmck;*kj0(_maSpV;>h9z^{i7^vzd$9z-qu{Kpk@(V+z{>t{N85@RTNdq7owmgL`U; zLZSj_zy`FpAW;E4&RYcP(1DhPK(ev|WE4gZ958y2uz;_CNGQ(EEJ#oQ&B|n^Wu_J@ zWaO7aT2wiyWvMyEAY+S55|gt*>q9_l70NR*b5a%3Q%g!fg*?PU*s=zYL-Z6t%4`i4 za#IuYiWQW~GeAAhoSb5X+*BnUh@=sUa3$2)N(!kZ$$D_7Ip!&(<`$GxDnK?6f|kZW z7FndGC}gIA4Jt@YQGmKLwIVq+HAMm9)(r5XBe1I=6K9~hrU=v|1NZf+IBXzcVpntw zR6KEm)7~x6AV6|<(HT%jj1!a&!P}c5Jql1EbBiM_9<u5Zl9$-h;z0`mL5)qwYz8Q= zX|hMLfC8Whw7vmc4n=W;=BD#X(uxvuQ^6xZx7Z-oM{$7kg8~gLv)|%|ge%zCtDtlX zD%BV`Ktm3oC2BlOprL9OMjj?E7RXXHE*2K1qEF~a8C1N$lQL+{8+hOb)N^B806rif zh6Oa}0O~G-1|FIji@ZR?50VTiOrROW8kPm1ZVR;MR^(N}Qp1|UoWjt|3|cW8%%I5v zsgS^d3Jw!ZXg20aPX#rRz%xn3;1IpVoSs@z)D8-6wkW2wB5;sHwmO2Ug(xnt+IUbd zF9IERfF2?uh`bG2@)!>e&7Yv40gc^)7Rd@Ri!kvpvas+l6@39&!+49?F|QI7>7W7% zj6vZ6YMFwwb`JySKmzda7N`Pef|OCUOeLU0LYP1^xwVk23~C`Wg3B?VT2Kd#6*S|Y z%~7OR!VU|M8b&yu1r$CtOdx&@%L0xXaJDYt1g$$sVa;YK(u1_63-xMPiaKgoQrILJ zprOg`hvHa}i$O&!xS{~XB4{~akt3+g0vFY21gPW&R}unDuu4KcJ+myew4fv-HLWx+ z8MAr-6|0~W7#|Os`A^TuPfE-wh7J#c^Nl7KIGUn(l1qz<QejE>J*YjwnHCS#03H~> z#gv{L#S4m~qEyg6dC(q8P-H`L4=77%vfW|@N7^kGkeyMiAnnCb93ZEGmK8*?f>_13 zSU?nd?DL}dgolZNp%^sJ2#R)4JC28ug%Q;I0xjSK4Iyzb7X5`J0dDBxIZ(R72whNx z1q$Wj7A(^^pecXwv^nUM7mg@4&}9D{M)0H!OB6d;hAV};g(Hds%mz*DbAT2FWPz3f zu%+<LVFK@aWQpQUkxT*Yd<5-;6iyLo0i75lnj!|?2`Qc;(ZUcV2;S`|6wIJ0b&ER` zG>#brUZ`6H+RgFu|NsC0Lr@m0fD!{J8H3yp4u=wkX3&WrjG)bm3z-?gUHW(?P@54p zje*!#$XQ&P3|_)r3?01#4`Zh$=HB7~wFN;dxj{#oK*mf#H8~_Mz{v|#3MqjCALIg1 zHCiQt<Xm{a3p_RsDXTG>J|Lao><2OnG!6=?RkIjDJ1W3DCUBzxHq6fK7scut6yzTi z#p34Z>7&UU#RpzA2;TSMlUZDHiw(St8I+C^GjmEo3(&Cj0=N;niq+sAVB=$~5=Zs` zk}8y;Mo>V591fzvD^5!oKzrzG7@9%DJB&3<5a(xsOO9UXIRGn}{oFJeZ?QwednGGm zb0>Hh8ZlT6j_6xli76>=5D(nqNluQ3_dQlZca?zFQQu-tEh;KPjgng&i7Aj>F#2$B zF^Vvv#tTdd<$fw+2em*sK?G<q0-9rrxEUE3j8WVnh8(;IPr{ZhgQjIsCL&PUk)TCV zpw=<$K#Usjj&Ml%2}{_ZHBXR2hKG@X0XnS@Sw@T~AVG;29LB+rv~L4<BNGpI6}lUt zics7IihWSq5u9|}KwIM&vKWflK%3rR{pb|%fL{q~78_{9fDu%uaMUuFfOgk#q%fCo zrGR#{aHp_JFqH6s_OVMaG&6}Xh%<l+7SIlt8pfii6plQm6wX@a8m1JkY}TTx8s-%4 z65bl7W=3&_6drK~a|W=9;Jq!N?5)Y`7g7Xj!4`oUGT<`%CCI7Zwhm|mX=+|d6ia|( zaBvhGC<D2=6kPzF{KKT6@rzTTI5jyxFQr&hlMmAO29+t0K^vyzWJvc3(dh&AwQmW4 zmJ2}k@<Nu0LPpZS8|lE~)S#077ISe)5wvhA;$Q?F##2(9np_;kRE$0qSOkhRUr=m- z=TyKe>p?q%wOBwyX(EhO63FQXIb)#24yefvkDYbU*a58wWdW^*Vl-!9Vu%OlSkOck zWJDP>K>!;40F60=m#Wy+fST-}5o{LFHs2IRaRyLZNSvXDC5>4OH15Hg!d%Om!cq$w zFJmiC0oDGXPCR(bgsm5}mXsq;3mo69DeNU&DI7IyDV!1vC7^v;tj&z@_~uIChQv4M zgaU9Z@<8?(Rq<(QX(<${X6mVCYAUD}zXYXQa3v73k{L2g2JU);#+wikUIbbc1&;6{ zAy5|Jjp6`JpA`p{<`w-1Rn=^e!O<wb^wbiURLEwX%>2At0>v5mMaUvW;-HKTHXYOt zE&{FhL5g5d+(xm+gACVXhqU>?#S&u{cz_^^3z0Y=D>%R@f)n0g0r}<@CwR;XG*1@; zigp=L{maD0$j8ja#08pCW8z|x01db^u`!A<X)#raA}0X2UX*GDl;A;07o0aiYd1k< z1|&|w1M;9LHl`G&X2_TqQx<bAQyxnSGpMBp>b8O6FomU;xfXPa3qw438I2|@Bt~xW z79ywiDg~R;JWzWbwDGC9L{A|&HB|w5Xxgp_v|dS*{gx1DO=?+YerYkttInW>v5)~R zP;0eF9u&^tAO;6ED4e1BB}xQ5_ZW}1p#rP{qzqCSgToXYoIH>TvS9GpMP;B61z7-E zp97x4U}9kwK+8nP8c>1}RJnmN5;z!ZpqU69j1{0&T;Rhv7{QBN8B;(@5f~9gT?y#$ z4aO|+21QWPPGPBKD&a_BE#XRGt6>6X0LCm%SQe;ZOkvMvEh<RiDB-DL1Q%_b;tY9A zDNN$vEr#GM!Q}@j*FcRnaJmNPgI|ma;6e^uiWP}~f>9Kl=8HfJwjc=>JnREX58$E< zeY6m~$rqf9z=aklO&3XkjDS?MQEZ?H&rOBQx`D$POn?KMFTN<XxFo+Q6%yP{px}m- zTcGn6Kr==H%q)yjj8gxrq>z&>sy5KkEW+Sn4Wv;ka8{89`3IaLK#ij!HV~H|M1bnm zB2dF2iX|B|*UbS=REfo@MFJpcP$60*2VyCL2xSlfUakmsI+y^r8bF=^7YTA8mxF3o z*hC-?3m+#B3lFD=AQwM}6bBDjl_ujY!T4~{kyAN&E~&}+MTsT(;Bp3(XW<x>Y(N}v zy9hR~0d5jAg4UZxF@pv}L1%V>8@Zqb<E*#%!Jz~41Z1)vB^`pI4wR)qUG8Gg1Qlph zHHRUWv6h(;v~UA7x&k?Li4nBBKa;VBX*v_=hz@YNfptki4T&NqP{gv_V#>@f0yz`X z00V_B@>wn5tz{5-kb3ks4M_Q9P<VklkDw*XY_JT4a6i1|i{cTG6F{B;XDE;{pcN^# zjF90YP;V1d6V)(ef!YPEpuGo>HVqq6l2rxOp4^aad*Jb#B4$wJg4Q#@nmS-NgX0)X zfa3TTcYJPQb}FP3F$3;)CeW~Vl{8w|>mjMd+*SZ`b`faZUlGm(rN+p>FbQM{DAG6> zd02};A{Ze7iU1HU2BkYh`lw|DtuwA+jAsCkmoj6nHU@<|c%d$6`5Nxw)(-I5f{?}` zXkg+A`d|pCFAc)r{xrx?P&EM^crAJgs--}sA6fwm?ols*_oy|>i$H63ix7S3+{E-u zEM4oOXQ2Kkble*>#tLeTrRC>Cj0Z;&D_HA%SjaIjN-<T5VK*Ptjfjs=$xj9ifEH<i zl7Kdd0LQo{hy^y09jbdC$V`wA85nt3I2em`LD_)$79V69B_68k7AH97<FivMZ?OjX z<R@oqGT!0>*KQ@H1v#mp^U;zsQa$t1@<B%emK5J&%grn<PA$I0o|{-voLQB6ivu(+ z0AitT(*Y#}P*j6(F=)mmg|U{YouQpEjR`zZ&H<`wLBmH?Q3}4G@PjT*fjSI4ZkJI~ zQcw&zGBcwTd|ak}L1jrsex80ZlupXYPtpg?bLjhICh3Dl4NCI!bBgr}D!~SzCL~Zk z1Vu0SSVPdxAy8*?Awvo%^?`F_4O0hWGjkeq8WXgQ$h?rT*aeiR(im$P=P}hV^+8Ua zVgc<H0`>RuxN1P>P=N+{S!zJrHkkaX*c`x%Y&6+!aex`|nJJKx9<=o07C&Tx3}}wI zBr!9u_!bvb0CbELj18Jsge*ycjE;fN`>6yaPtd^=E1AI2e~T3~WRikNP2f0Q4~lot z^Z)~+3=0>d03#o0UP}z@5=4rHhB8>MCJWfOA{$T|G6fN!v;vkYvIcR%>53I>!YYvI z;8exJ!pO&61X^YZs+~cFKOBR?8N@06gFd7J+BO8*LJ2;@Ac`%OJq0wd0$K^nk;)G0 z1A#`|qBy}N1Ly!<jwmiL8#2hk4Q4|IS$LqkCwNo&!0Ti|X7H!-X9=VTfez4Z2CbWg z473QQKn7ZbQXm5@!eCP*Q>0orqC_A^<3@?5=z|Z&WsDMo9*ipvITklcB1JhxrG+6% zGQ}WODpe|lF-2_-bBcP3Mhk0{H26SVhA0{6fw;0Mx+!`s3{i66T24NgLDTRSXDFzK z04+{KO^cvH2$VIz^#UlmN<e)@@IJO+2Jp&4@X>&pOyD!SAZNy58`NNj9+!6r6fvO6 zhJmq45N@X)R2HQm23ZYK3AP$ko`TB26vk}kA_Wi&G#CnM=cO=7f;ZCzgB%80hr(PX ztq=s+KMI;yfGr4C053aMfQ$lbvJ~ksGBALK(Tg$*z-w}fK&z})GGd*q;e#0hX<#3P zdx9C<=_@V)1)LtLBHVtF0}tkcCb>a10O(*w@KiMum;@j82|AJ?7`|}CQz0=IyhFaY zv?x^p5oqA0$r_rV$%eet)Rfc|g~Ytd-29?cg~YrRh0GF#;?&gKVuiBQBG9P_$=Q{9 znk?Wh(k%|qw%O$T+*I(YvRmwstZh^TS{Dq-P~g%Ul%a}13%O2#JPhizf>siO1~IEZ zts<zm!OlSOEhyGN7?f~|K?i|>8WW)7Ou<9?%%GhCpzdr5=txS|W(LUO1yG9!JYuZL zj?}0C#e0!DD0hPl12>AISc_9jLJMGR3a})&$q08dxU9;_2?i;<2zNSk`6GC^3F>t4 zoFP~liu++H4`d~{P5`Ah(9x2h<0L^Y2gMt-Hr8Z?BznZ)18DdX)U8b{a>*~xgY0-k zb006*;*eB`k{ci=g6bjA5HWa=57~JzRVXe5g)Rt#T?p!zLPk|VGk@S6dQ3$Ups`6% zNW<L83=X^^8Bnt42X~BpQ$c&u{1S6hp<#K8sW=6#NqI{Gz2hBRl9-p0Sd?-X<Z5u5 z0B^8hW2{m_Bu!kJP&@&0JjmPN3=V2ML+UwjYm*T&ewM<R#spbQ4qo5PT%=LM6bx>C zfljjnb&WKci#$Qe!wN)z7V$%OC>42t1VAd#6Er9U9)lbYt{=cBfq}a~&_G9bI*OBF zDFf_ekmn&zhJ`UBICvpZ4o;0=0-TLNE&-jsRt$<Z1}5YT1TL6R#)v>_;2{HQgMzGq z>=}lI2s8<LftpK9MV27Xf_z^Dnkho}G05(>ARmLw#j;5VBn<WuD7S&qI>?eDF%S!U z7$H-UTqdY=g&aJ{ki`ft`iq<)c7r<cU^X^?-{JwEsFz<_0xE7kf*b)3c1AJK*~6gG zOlYtpD?<qjP%RC@U<ZQsHIy)bR&s(y%s^EDBgkP)DGb@nMMmIJG4SenkOx35o`npc z)(_b6pq&XU3mH<F(m+d-8Jj_)lVFo-m?RiLt3aVU7r^>$7?8|jP6HhU1{>yQ@hbv_ z=PfqKv1H&S%bFZu>Xt}+Y96SNhLoqEQ&5Xbilg|Da+^zjGE!uLQ!Z#PK@n&%JhY=1 z#RVF*f@Ds}Kw3U%WQ-lUWCksA!40*X98m2I(ee!x#*m&qXbP5#3ADJLjgbd5xFU(j zq0kbjxCEjLC6$9RCOl1m+Qg8Q0LqwIOy~!UGlM5n!L?_RJ19j!1}APYCf{O2KbEfu zltkH?p*w#;#U1EGScId%ZMXQ$6b#pZTm<f8gZg-&vM!4eG#m!%Nka+>@ZK`mQQ4q* zM@<$;c?Z&Pi#r~)lpYc*w}j&%F$dwsXXd4(R=^4kP-O+_W22VX;4?vZ5Do{erLPh~ zI2?X>HmHCAwJAXu#0EJWoN__uiNM-U&5+`mF$<J#nTnJ^)e30XDxL|d0yO?-1ro_( z0S{`qz*Vq-s!7mx1kiHuB2d!NWJMaR1Brr8MNBRM%VEorMZSy-3_>8UfXXk>LBw33 zqgCXPJcA_>W1Wrxdkj`QftJ{Toy-ItDqva&KZ2eGY$YU_fE-u^I)Wb-;-KEbEq3V4 zCtBJr0u|~KAg6)~Lr`)-tGtslzz23>I1V1_D2@Y7*D+=>)iBy1?I&eMavV4~A<4H0 z<gOx6F9zLBprTfWfSU?ROHj%%P-X&QaDagpkAcc_aDaivdl|Esve{r&2(o)Xfd#Fa zA@1P>b*;bzxK6QPWMEK0aSCb%2L(@Zejd30h7v{~BS2*;=q$%#P>lg`52(}x?I~Ko z2wDkHtWW}4dCLkuWeqgnz?unKT>+X=V5w!VVXtA!W-sz60UdG3n!-}U-ps<tP{X!> zwT2xu$5_h&%AlaL$!eJAF+olouVJcTt6>M1t!y={HEf`=xrTv-A&Vu26+G_&N!@II zMLwV<)l%>lFnBH=w7p0H)T__TORrMHEeGwOgY$J2OJ<6KEo6ZpJGk^I0tFhl6%0;Y zMZut&6O<sq#j++Rc&;D{dVFX+=y+V#w9KO767&u(s7HB=DKjOCGah!-3R+6J#SQAc zLJJ!eP&9#3BWP3(G6D-4;Q}oL0G-Rk!w8-{fYyab`t&rJi^4!o^aVvdcnT7fxIx7W z2!q(5h(t+Ppj-$Fe9-JVsJMU()iD)CfXo4%(h19Z2!lZh0^)p7!n(x+DIvk<|2i9i zoDNF842<CEN2nu_m7tW@pkfd1CeW^XQ0spI6L>V90km>#AtQLgiXn?Ni%pyXG?E_7 zpvml4#f`XBM8AqfuOK@|lf4MOTMe>F4Yacovc(bH>;TmX*b2E@+|be;w35^m$pfG% z8mI@5RG_#Y6sDl?0Ea1P^(rKD)G~sm(HAg-NfyvXkA;lIdL@htSdm2-8Nh33*}?mw z5=uBg`^%b{`o(IQpzCGXv$#Ro19X&@DFgKA7tnHKW<PL=eT%6)Nt3523KUgvuis+L zEC%h!18vj0#Q_>FP01`OMil$ESn>-}^KP*fWF}|lq~2mlDa|ban+A>OTdc{TaJa>Y zetiabtCR&OctBlyQ0GpBQHY5Lbfyb(Kp{40p#&7DP=*H-=ol?fGXdNZ;{zo-NP87D zhMdK`032Z8?IWPo-Aw%wwM->!3qU0WBWQpPw5u^5d@ds-Nb<N!I8&HFry4-oxth#= zx0s5Os$`(qUQr<>u_RFe<STH`7<`11CKtFniynAvsTG;UCB;Q?pfCmPiMz!DY7Ic@ zBhVBJbULdj5u_jq)IVYYnGVShAP+(o;RZ1>FgSz43cQa3wB?400~}Udj3SV^KUl&7 z84pQa;1kO9G!-BNYoPoOX()l3&Jf3f>YO5bkj>z2Q%)cjsLNgC3Szl|2yiPA)N3gM zt?&f5K8vP;iUUxivB)2!1k|nqw^1Oq8K~X_muHaD4xI77f^<ZJ6oV5Nc#|B+OSlKi z?HCyt_JUfIpu>TnvoA~>pxx&zOpIJ?JnTHIJZv0npzT;Jpe<Rf9D*ER5iS<cbr>9c zBFr4j9Na}9b5IfvsDuDva1H~dBKXA&;4s%@ECP+lf!PoOR49S3Y6$>^Kcp-I^D&GC zWnK_22JN4R4&#F7b~-_$rwkp8@eH6-MLHPc89PAT80M<A;1O8RL1&Obqhf_bh2;E# zO3-T3wEUvf%yh(rl8yrW<g?6_)Vz|+<is4%0@hT}7z^B-^2ACFElnK-&@#`05{1N^ z;(P_rNPJ!j$ZWk7*z}YF?34h|dR5R+6=)%4Vp2}3LS_lvu;PNul48)wVtQa96c2*} z0))Xc4xlDKXb8BOv6iue5jIHyDf=03al&oYWCDloEykdgOt%EkeRzu}FSQ&#frb?N zpbe0bpwI_*-<UZV*_f*IKt)kdYI-K<N{r0Bbhtjm04}m=noN*v02+DIWGu=8rA2V| z$^@}MaePYxmx;k3tHBLcHa^hOQl_FDMg|6yMiwaAL5T@8&RYz+K>$=7fx3t;46$n9 zmC%sgs$4ao4K$3|OhsHJETH8opk1qB42%qgT#zFR6SpuToqY#S6*)NykOj%$O{mU3 z!QjKw6<l1M{ev7s{DVN})|F(Wf};tPobpRSE2Hv@OBCQ?k618_q%F6!xI`f-RUuJ9 zsR$942u(`X3i){mW0Mo}6hNT_8tg+TLeZ<FP?nhpI_3`U7LW#P5v2g0ipb2*Lvk5p z=RN2~FwpTga3gX+hvI<_S<--pLU?9L20X?!phv{z7r{>1&;dJ66LI<-=ybU>P;SX7 zwt|}q3MlYOHVx=;J9_D<CB^xrMaik)t&kdNnhKf<$_gI&<)B=W4q8K=lcSK43A#TD ze&kRwBy$v{GcqvvX|fi*26dR(z^inj)g(v;-0IV0hK%rn1fuwI6DyNap`J*EPEml9 zBsevSVDo7PC?$fV8TddO=|Fd3fX?^@otpz%+yvS}>j)lPhD?I9=9T6aR6@>A2ageg z$16c;wWtJCBA)^g*FnSs5CPhgQUtD~I6>DZfoF-pZJ=B1xs~8s(QdKi7MJGUV#|eG z8V1Vt;LE9QvE?Uar6!kvZ>=c;t%-n~2M5XtMW7R1z-JbL4^acJf(4%}1wKCnyn`0J zV-&m_5WKG>iVw6@1T+w#R}9)`UIe<v5WJNFvJ(Ncd>g!U`4(GAC1?v(5x9N<H_t#v zSb^8H7X1Txi3^mwL5q8$_~B-PHsYlg6@jLVi?)Llfu>=>^Fp^c!ATM{I|f>Q%uocH zjVdw#nPCX3nz`bgK|94kGkxG$9dPXet_#2=0yrul#TGao!2~F_Zo${lgKC~)&~7Le zR?q+m=u|HV4JScsdl)&GIN14E1b79c1^9*P8CV5m1h|E?`FuF|IJo%?dDu8uIM}#U pI0SeY1(-Mlc$kC=c^G+^gm^eOK>M_Wn7D+6>KQ~hm^p+wm;izo$3Fl7 diff --git a/unitgrade2/__pycache__/unitgrade_helpers2.cpython-38.pyc b/unitgrade2/__pycache__/unitgrade_helpers2.cpython-38.pyc deleted file mode 100644 index efd8ce6f58cc6a9f0d3cd6104d4cbe6e9982131d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6959 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 diff --git a/unitgrade2/__pycache__/version.cpython-38.pyc b/unitgrade2/__pycache__/version.cpython-38.pyc deleted file mode 100644 index 1e804a3c2f2261406205db1c73302a4acb70b698..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167 zcmWIL<>g{vU|`sqpqMDiz`*br#6iZ43=9ko3=9m#91IK$DGX5zDU87knoL!!26~oy z27a21x47fu%TkMqGxPJ}<5w~iF)=Veh+pQ;RxzQ)sYS&xC8ZguF)sPZrManjCB-qN ld6^~YMTse?2$oR{)C|3X%3B;Zx%nxjIjMFa%Rhr$0|4qjDn$SQ diff --git a/unitgrade2/__pycache__/version.cpython-39.pyc b/unitgrade2/__pycache__/version.cpython-39.pyc deleted file mode 100644 index 613bef2848d43d5e78803c81d3d366c27a5e2c62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 164 zcmYe~<>g{vU|>*k(@Yd)U|@I*;vi#21_lNP1_p*=4h9B>6ox2<6vki%O{OYV13gPU z13yj1Tio&SWvNBQnfZC~@hcgMm>3u!#4i*5jQreG{gTp*RDGBH<kH;Kypm%5(!9)) i^rFO+R0PXNA8LSJLFFwDo80`A(wtN~kj<Y#P5}VquqU+u diff --git a/unitgrade2/unitgrade/ListQuestion.pkl b/unitgrade2/unitgrade/ListQuestion.pkl deleted file mode 100644 index b201d4b453d94b66ed44ac821d72534a90b09811..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 466 zcmZo*nR=X&0Ss!VX!Mx*WEPhMmZlb$Waj7ThK8gTmpCUDr|QBP`AJ!+$t6}Lsl_Gn znRz9t>8VA<R)$kzr)YRHdoy`6rjlud5y*%hj>O`WwBp2+v?)DIM#fW8dw7uDGo^<Y z#oj624DBFoP@6N@GdR2%!6Fz2f#lkzfPBbWl39|II;BSdWRwET3wnBbQ+ilRGILX> zxZ5vGT>uJ@(hT+<9+0Z|;*z4wy!7HJU=uQg-9fS-oFUkwTLA_Um_Pxn6=s2MaYlYo d$rO;8!eEKq{GwD?2o_;A+9(+>z;1r29ssAsrmFw| diff --git a/unitgrade2/unitgrade2.py b/unitgrade2/unitgrade2.py deleted file mode 100644 index c094340..0000000 --- a/unitgrade2/unitgrade2.py +++ /dev/null @@ -1,891 +0,0 @@ -""" -git add . && git commit -m "Options" && git push && pip install git+ssh://git@gitlab.compute.dtu.dk/tuhe/unitgrade.git --upgrade - -""" -# from . import cache_read -import unittest -import numpy as np -import sys -from io import StringIO -import collections -import re -import threading -import tqdm -import time -import pickle -import itertools -import os - -myround = lambda x: np.round(x) # required. -msum = lambda x: sum(x) -mfloor = lambda x: np.floor(x) - -def setup_dir_by_class(C,base_dir): - name = C.__class__.__name__ - # base_dir = os.path.join(base_dir, name) - # if not os.path.isdir(base_dir): - # os.makedirs(base_dir) - return base_dir, name - -class Hidden: - def hide(self): - return True - -class Logger(object): - def __init__(self, buffer): - self.terminal = sys.stdout - self.log = buffer - - def write(self, message): - self.terminal.write(message) - self.log.write(message) - - def flush(self): - # this flush method is needed for python 3 compatibility. - pass - -class Capturing(list): - def __init__(self, *args, 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 if self._stdout == None else self._stdout - self._stringio = StringIO() - if self.unmute: - sys.stdout = Logger(self._stringio) - else: - sys.stdout = self._stringio - - if capture_errors: - self._sterr = sys.stderr - sys.sterr = StringIO() # memory hole it - self.capture_errors = capture_errors - return self - - def __exit__(self, *args): - self.extend(self._stringio.getvalue().splitlines()) - del self._stringio # free up some memory - sys.stdout = self._stdout - if self.capture_errors: - sys.sterr = self._sterr - -class 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 - testfun = None - tol = 0 - estimated_time = 0.42 - _precomputed_payload = None - _computed_answer = None # Internal helper to later get results. - weight = 1 # the weight of the question. - - def __init__(self, question=None, *args, **kwargs): - if self.tol > 0 and self.testfun is None: - self.testfun = self.assertL2Relative - elif self.testfun is None: - self.testfun = self.assertEqual - - self.name = self.__class__.__name__ - # self._correct_answer_payload = correct_answer_payload - self.question = question - - super().__init__(*args, **kwargs) - if self.title is None: - self.title = self.name - - def _safe_get_title(self): - if self._precomputed_title is not None: - return self._precomputed_title - return self.title - - def assertNorm(self, computed, expected, tol=None): - if tol == None: - tol = self.tol - diff = np.abs( (np.asarray(computed).flat- np.asarray(expected)).flat ) - nrm = np.sqrt(np.sum( diff ** 2)) - - self.error_computed = nrm - - if nrm > tol: - print(f"Not equal within tolerance {tol}; norm of difference was {nrm}") - print(f"Element-wise differences {diff.tolist()}") - self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol}") - - def assertL2(self, computed, expected, tol=None): - if tol == None: - tol = self.tol - diff = np.abs( (np.asarray(computed) - np.asarray(expected)) ) - self.error_computed = np.max(diff) - - if np.max(diff) > tol: - print(f"Not equal within tolerance {tol=}; deviation was {np.max(diff)=}") - print(f"Element-wise differences {diff.tolist()}") - self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol=}, {np.max(diff)=}") - - def assertL2Relative(self, computed, expected, tol=None): - if tol == None: - tol = self.tol - diff = np.abs( (np.asarray(computed) - np.asarray(expected)) ) - diff = diff / (1e-8 + np.abs( (np.asarray(computed) + np.asarray(expected)) ) ) - self.error_computed = np.max(np.abs(diff)) - if np.sum(diff > tol) > 0: - print(f"Not equal within tolerance {tol}") - print(f"Element-wise differences {diff.tolist()}") - self.assertEqual(computed, expected, msg=f"Not equal within tolerance {tol}") - - def precomputed_payload(self): - return self._precomputed_payload - - def precompute_payload(self): - # Pre-compute resources to include in tests (useful for getting around rng). - pass - - def compute_answer(self, unmute=False): - raise NotImplementedError("test code here") - - def test(self, computed, expected): - self.testfun(computed, expected) - - def get_points(self, verbose=False, show_expected=False, show_computed=False,unmute=False, passall=False, silent=False, **kwargs): - possible = 1 - computed = None - def show_computed_(computed): - print(">>> Your output:") - print(computed) - - def show_expected_(expected): - print(">>> Expected output (note: may have been processed; read text script):") - print(expected) - - correct = self._correct_answer_payload - try: - if unmute: # Required to not mix together print stuff. - print("") - computed = self.compute_answer(unmute=unmute) - except Exception as e: - if not passall: - if not silent: - print("\n=================================================================================") - print(f"When trying to run test class '{self.name}' your code threw an error:", e) - show_expected_(correct) - import traceback - print(traceback.format_exc()) - print("=================================================================================") - return (0, possible) - - if self._computed_answer is None: - self._computed_answer = computed - - if show_expected or show_computed: - print("\n") - if show_expected: - show_expected_(correct) - if show_computed: - show_computed_(computed) - try: - if not passall: - self.test(computed=computed, expected=correct) - except Exception as e: - if not silent: - print("\n=================================================================================") - print(f"Test output from test class '{self.name}' does not match expected result. Test error:") - print(e) - show_computed_(computed) - show_expected_(correct) - return (0, possible) - return (1, possible) - - def score(self): - try: - self.test() - except Exception as e: - return 0 - return 1 - -class QPrintItem(QItem): - def compute_answer_print(self): - """ - Generate output which is to be tested. By default, both text written to the terminal using print(...) as well as return values - are send to process_output (see compute_answer below). In other words, the text generated is: - - res = compute_Answer_print() - txt = (any terminal output generated above) - numbers = (any numbers found in terminal-output txt) - - self.test(process_output(res, txt, numbers), <expected result>) - - :return: Optional values for comparison - """ - raise Exception("Generate output here. The output is passed to self.process_output") - - def process_output(self, res, txt, numbers): - return res - - def compute_answer(self, unmute=False): - with Capturing(unmute=unmute) as output: - res = self.compute_answer_print() - s = "\n".join(output) - s = rm_progress_bar(s) # Remove progress bar. - numbers = extract_numbers(s) - self._computed_answer = (res, s, numbers) - return self.process_output(res, s, numbers) - -class OrderedClassMembers(type): - @classmethod - def __prepare__(self, name, bases): - return collections.OrderedDict() - def __new__(self, name, bases, classdict): - ks = list(classdict.keys()) - for b in bases: - ks += b.__ordered__ - classdict['__ordered__'] = [key for key in ks if key not in ('__module__', '__qualname__')] - return type.__new__(self, name, bases, classdict) - -class QuestionGroup(metaclass=OrderedClassMembers): - title = "Untitled question" - partially_scored = False - t_init = 0 # Time spend on initialization (placeholder; set this externally). - estimated_time = 0.42 - has_called_init_ = False - _name = None - _items = None - - @property - def items(self): - if self._items == None: - self._items = [] - members = [gt for gt in [getattr(self, gt) for gt in self.__ordered__ if gt not in ["__classcell__", "__init__"]] if inspect.isclass(gt) and issubclass(gt, QItem)] - for I in members: - self._items.append( I(question=self)) - return self._items - - @items.setter - def items(self, value): - self._items = value - - @property - def name(self): - if self._name == None: - self._name = self.__class__.__name__ - return self._name # - - @name.setter - def name(self, val): - self._name = val - - def init(self): - # Can be used to set resources relevant for this question instance. - pass - - def init_all_item_questions(self): - for item in self.items: - if not item.question.has_called_init_: - item.question.init() - item.question.has_called_init_ = True - - -class Report(): - title = "report title" - version = None - questions = [] - pack_imports = [] - individual_imports = [] - nL = 80 # Maximum line width - - @classmethod - def reset(cls): - for (q,_) in cls.questions: - if hasattr(q, 'reset'): - q.reset() - - @classmethod - def mfile(clc): - return inspect.getfile(clc) - - def _file(self): - return inspect.getfile(type(self)) - - def _import_base_relative(self): - root_dir = self.pack_imports[0].__path__._path[0] - root_dir = os.path.dirname(root_dir) - relative_path = os.path.relpath(self._file(), root_dir) - 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) - # else: - # if os.path.isfile(self.computed_answers_file): - # self.set_payload(cache_read(self.computed_answers_file), strict=strict) - # else: - # s = f"> Warning: The pre-computed answer file, {os.path.abspath(self.computed_answers_file)} is missing. The framework will NOT work as intended. Reasons may be a broken local installation." - # if strict: - # raise Exception(s) - # else: - # print(s) - - def main(self, verbosity=1): - # Run all tests using standard unittest (nothing fancy). - import unittest - loader = unittest.TestLoader() - for q,_ in self.questions: - import time - start = time.time() # A good proxy for setup time is to - suite = loader.loadTestsFromTestCase(q) - unittest.TextTestRunner(verbosity=verbosity).run(suite) - total = time.time() - start - q.time = total - - def _setup_answers(self): - self.main() # Run all tests in class just to get that out of the way... - report_cache = {} - for q, _ in self.questions: - if hasattr(q, '_save_cache'): - q()._save_cache() - q._cache['time'] = q.time - report_cache[q.__qualname__] = q._cache - else: - report_cache[q.__qualname__] = {'no cache see _setup_answers in unitgrade2.py':True} - return report_cache - - def set_payload(self, payloads, strict=False): - for q, _ in self.questions: - q._cache = payloads[q.__qualname__] - -def rm_progress_bar(txt): - # More robust version. Apparently length of bar can depend on various factors, so check for order of symbols. - nlines = [] - for l in txt.splitlines(): - pct = l.find("%") - ql = False - if pct > 0: - i = l.find("|", pct+1) - if i > 0 and l.find("|", i+1) > 0: - ql = True - if not ql: - nlines.append(l) - return "\n".join(nlines) - -def extract_numbers(txt): - # txt = rm_progress_bar(txt) - numeric_const_pattern = '[-+]? (?: (?: \d* \. \d+ ) | (?: \d+ \.? ) )(?: [Ee] [+-]? \d+ ) ?' - rx = re.compile(numeric_const_pattern, re.VERBOSE) - all = rx.findall(txt) - all = [float(a) if ('.' in a or "e" in a) else int(a) for a in all] - if len(all) > 500: - print(txt) - raise Exception("unitgrade.unitgrade.py: Warning, 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 - self._running = False - self.title = title - self.dt = 0.1 - self.n = int(np.round(self.t / self.dt)) - self.show_progress_bar = show_progress_bar - - # self.pbar = tqdm.tqdm(total=self.n) - if start: - self.start() - - def start(self): - self._running = True - if self.show_progress_bar: - self.thread = threading.Thread(target=self.run) - self.thread.start() - self.time_started = time.time() - - def terminate(self): - if not self._running: - raise Exception("Stopping a stopped progress bar. ") - self._running = False - if self.show_progress_bar: - self.thread.join() - if hasattr(self, 'pbar') and self.pbar is not None: - self.pbar.update(1) - self.pbar.close() - self.pbar=None - - sys.stdout.flush() - return time.time() - self.time_started - - def run(self): - self.pbar = tqdm.tqdm(total=self.n, file=sys.stdout, position=0, leave=False, desc=self.title, ncols=100, - bar_format='{l_bar}{bar}| [{elapsed}<{remaining}]') # , unit_scale=dt, unit='seconds'): - - for _ in range(self.n-1): # Don't terminate completely; leave bar at 99% done until terminate. - if not self._running: - self.pbar.close() - self.pbar = None - break - - time.sleep(self.dt) - self.pbar.update(1) - - - -from unittest.suite import _isnotsuite - -# class MySuite(unittest.suite.TestSuite): # Not sure we need this one anymore. -# raise Exception("no suite") -# pass - -def instance_call_stack(instance): - s = "-".join(map(lambda x: x.__name__, instance.__class__.mro())) - return s - -def get_class_that_defined_method(meth): - for cls in inspect.getmro(meth.im_class): - if meth.__name__ in cls.__dict__: - return cls - return None - -def caller_name(skip=2): - """Get a name of a caller in the format module.class.method - - `skip` specifies how many levels of stack to skip while getting caller - name. skip=1 means "who calls me", skip=2 "who calls my caller" etc. - - An empty string is returned if skipped levels exceed stack height - """ - stack = inspect.stack() - start = 0 + skip - if len(stack) < start + 1: - return '' - parentframe = stack[start][0] - - name = [] - module = inspect.getmodule(parentframe) - # `modname` can be None when frame is executed directly in console - # TODO(techtonik): consider using __main__ - if module: - name.append(module.__name__) - # detect classname - if 'self' in parentframe.f_locals: - # I don't know any way to detect call from the object method - # XXX: there seems to be no way to detect static method call - it will - # be just a function call - name.append(parentframe.f_locals['self'].__class__.__name__) - codename = parentframe.f_code.co_name - if codename != '<module>': # top level usually - name.append( codename ) # function or a method - - ## Avoid circular refs and frame leaks - # https://docs.python.org/2.7/library/inspect.html#the-interpreter-stack - del parentframe, stack - - return ".".join(name) - -def get_class_from_frame(fr): - import inspect - args, _, _, value_dict = inspect.getargvalues(fr) - # we check the first parameter for the frame function is - # named 'self' - if len(args) and args[0] == 'self': - # in that case, 'self' will be referenced in value_dict - instance = value_dict.get('self', None) - if instance: - # return its class - # isinstance(instance, Testing) # is the actual class instance. - - return getattr(instance, '__class__', None) - # return None otherwise - return None - -from typing import Any -import inspect, gc - -def giveupthefunc(): - frame = inspect.currentframe() - code = frame.f_code - globs = frame.f_globals - functype = type(lambda: 0) - funcs = [] - for func in gc.get_referrers(code): - if type(func) is functype: - if getattr(func, "__code__", None) is code: - if getattr(func, "__globals__", None) is globs: - funcs.append(func) - if len(funcs) > 1: - return None - return funcs[0] if funcs else None - - -from collections import defaultdict - -class UTextResult(unittest.TextTestResult): - nL = 80 - number = -1 # HAcky way to set question number. - show_progress_bar = True - def __init__(self, stream, descriptions, verbosity): - super().__init__(stream, descriptions, verbosity) - self.successes = [] - - def printErrors(self) -> None: - # if self.dots or self.showAll: - # self.stream.writeln() - # if hasattr(self, 'cc'): - # self.cc.terminate() - # self.cc_terminate(success=False) - self.printErrorList('ERROR', self.errors) - self.printErrorList('FAIL', self.failures) - - def addError(self, test, err): - super(unittest.TextTestResult, self).addFailure(test, err) - self.cc_terminate(success=False) - - def addFailure(self, test, err): - super(unittest.TextTestResult, self).addFailure(test, err) - self.cc_terminate(success=False) - # if self.showAll: - # self.stream.writeln("FAIL") - # elif self.dots: - # self.stream.write('F') - # self.stream.flush() - - def addSuccess(self, test: unittest.case.TestCase) -> None: - # super().addSuccess(test) - self.successes.append(test) - # super().addSuccess(test) - # hidden = issubclass(item.__class__, Hidden) - # # if not hidden: - # # print(ss, end="") - # # sys.stdout.flush() - # start = time.time() - # - # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent) - # q_[j] = {'w': item.weight, 'possible': possible, 'obtained': current, 'hidden': hidden, 'computed': str(item._computed_answer), 'title': item.title} - # tsecs = np.round(time.time()-start, 2) - self.cc_terminate() - - - - def cc_terminate(self, success=True): - if self.show_progress_bar or True: - tsecs = np.round(self.cc.terminate(), 2) - sys.stdout.flush() - ss = self.item_title_print - print(self.item_title_print + ('.' * max(0, self.nL - 4 - len(ss))), end="") - # current = 1 - # possible = 1 - # current == possible - ss = "PASS" if success else "FAILED" - if tsecs >= 0.1: - ss += " (" + str(tsecs) + " seconds)" - print(ss) - - - def startTest(self, test): - # super().startTest(test) - j =self.testsRun - self.testsRun += 1 - # print("Starting the test...") - # show_progress_bar = True - n = UTextResult.number - - item_title = self.getDescription(test) - # 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 - nL = 80 - # - if self.show_progress_bar or True: - self.cc = ActiveProgress(t=estimated_time, title=self.item_title_print, show_progress_bar=self.show_progress_bar) - else: - print(self.item_title_print + ('.' * max(0, nL - 4 - len(self.item_title_print))), end="") - - self._test = test - - def _setupStdout(self): - if self._previousTestClass == None: - total_estimated_time = 1 - if hasattr(self.__class__, 'q_title_print'): - q_title_print = self.__class__.q_title_print - else: - q_title_print = "<unnamed test. See unitgrade.py>" - - # q_title_print = "some printed title..." - cc = ActiveProgress(t=total_estimated_time, title=q_title_print, show_progress_bar=self.show_progress_bar) - self.cc = cc - - def _restoreStdout(self): # Used when setting up the test. - if self._previousTestClass == None: - q_time = self.cc.terminate() - q_time = np.round(q_time, 2) - sys.stdout.flush() - print(self.cc.title, end="") - # start = 10 - # q_time = np.round(time.time() - start, 2) - nL = 80 - print(" " * max(0, nL - len(self.cc.title)) + ( - " (" + str(q_time) + " seconds)" if q_time >= 0.1 else "")) # if q.name in report.payloads else "") - # print("=" * nL) - -from unittest.runner import _WritelnDecorator -from io import StringIO - -class UTextTestRunner(unittest.TextTestRunner): - def __init__(self, *args, **kwargs): - from io import StringIO - stream = StringIO() - super().__init__(*args, stream=stream, **kwargs) - - def _makeResult(self): - # stream = self.stream # not you! - stream = sys.stdout - stream = _WritelnDecorator(stream) - return self.resultclass(stream, self.descriptions, self.verbosity) - -def wrapper(foo): - def magic(self): - s = "-".join(map(lambda x: x.__name__, self.__class__.mro())) - # print(s) - foo(self) - magic.__doc__ = foo.__doc__ - return magic - -from functools import update_wrapper, _make_key, RLock -from collections import namedtuple -_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) - -def cache(foo, typed=False): - """ Magic cache wrapper - https://github.com/python/cpython/blob/main/Lib/functools.py - """ - maxsize = None - def wrapper(self, *args, **kwargs): - key = (self.cache_id(), ("@cache", 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) - else: - value = self._cache_get(key) - return value - return wrapper - - -class UTestCase(unittest.TestCase): - _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache. - _cache = None # Read-only cache. 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 = {} - return self.__class__._outcome - - 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 - # 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 __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._load_cache() - 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: - self.__class__._cache = dict() - if not hasattr(self.__class__, '_cache2') or self.__class__._cache2 == None: - self.__class__._cache2 = dict() - - def _cache_get(self, key, default=None): - self._ensure_cache_exists() - return self.__class__._cache.get(key, default) - - def _cache_put(self, key, value): - self._ensure_cache_exists() - self.__class__._cache2[key] = value - - def _cache_contains(self, key): - self._ensure_cache_exists() - return key in self.__class__._cache - - def 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: - self.wrap_assert(self.assertEqual, first, msg) - - def _cache_file(self): - return os.path.dirname(inspect.getfile(self.__class__) ) + "/unitgrade/" + self.__class__.__name__ + ".pkl" - - def _save_cache(self): - # get the class name (i.e. what to save to). - cfile = self._cache_file() - if not os.path.isdir(os.path.dirname(cfile)): - os.makedirs(os.path.dirname(cfile)) - - if hasattr(self.__class__, '_cache2'): - with open(cfile, 'wb') as f: - pickle.dump(self.__class__._cache2, f) - - # But you can also set cache explicitly. - def _load_cache(self): - if self._cache != None: # Cache already loaded. We will not load it twice. - return - # raise Exception("Loaded cache which was already set. What is going on?!") - cfile = self._cache_file() - # print("Loading cache from", cfile) - if os.path.exists(cfile): - with open(cfile, 'rb') as f: - data = pickle.load(f) - self.__class__._cache = data - else: - print("Warning! data file not found", cfile) - -def hide(func): - return func - -def makeRegisteringDecorator(foreignDecorator): - """ - Returns a copy of foreignDecorator, which is identical in every - way(*), except also appends a .decorator property to the callable it - spits out. - """ - def newDecorator(func): - # Call to newDecorator(method) - # Exactly like old decorator, but output keeps track of what decorated it - R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done - R.decorator = newDecorator # keep track of decorator - # R.original = func # might as well keep track of everything! - return R - - newDecorator.__name__ = foreignDecorator.__name__ - newDecorator.__doc__ = foreignDecorator.__doc__ - # (*)We can be somewhat "hygienic", but newDecorator still isn't signature-preserving, i.e. you will not be able to get a runtime list of parameters. For that, you need hackish libraries...but in this case, the only argument is func, so it's not a big issue - return newDecorator - -hide = makeRegisteringDecorator(hide) - -def methodsWithDecorator(cls, decorator): - """ - Returns all methods in CLS with DECORATOR as the - outermost decorator. - - DECORATOR must be a "registering decorator"; one - can make any decorator "registering" via the - makeRegisteringDecorator function. - - import inspect - ls = list(methodsWithDecorator(GeneratorQuestion, deco)) - for f in ls: - print(inspect.getsourcelines(f) ) # How to get all hidden questions. - """ - for maybeDecorated in cls.__dict__.values(): - if hasattr(maybeDecorated, 'decorator'): - if maybeDecorated.decorator == decorator: - print(maybeDecorated) - yield maybeDecorated - diff --git a/unitgrade2/version.py b/unitgrade2/version.py deleted file mode 100644 index acb984f..0000000 --- a/unitgrade2/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.9.0" \ No newline at end of file -- GitLab