diff options
author | jwansek <eddie.atten.ea29@gmail.com> | 2022-04-22 16:02:28 +0100 |
---|---|---|
committer | jwansek <eddie.atten.ea29@gmail.com> | 2022-04-22 16:02:28 +0100 |
commit | 12ac9bd354c664978523547ffac9bbebf0fcb577 (patch) | |
tree | eb9c3726edfa291d0b6c53cec0f25572946fb887 /Smarker | |
parent | 5faf5c34d833f8467bce580e1e249f54856042dc (diff) | |
download | Smarker-12ac9bd354c664978523547ffac9bbebf0fcb577.tar.gz Smarker-12ac9bd354c664978523547ffac9bbebf0fcb577.zip |
Moved and adapted to move folder, added rendering to pdfs
Diffstat (limited to 'Smarker')
-rw-r--r-- | Smarker/database.py | 97 | ||||
-rw-r--r-- | Smarker/jinja_helpers.py | 137 | ||||
-rw-r--r-- | Smarker/mark.py | 105 | ||||
-rw-r--r-- | Smarker/misc_classes.py | 99 | ||||
-rw-r--r-- | Smarker/pytest_template.jinja2 | 8 | ||||
m--------- | Smarker/python-latex-highlighting | 0 | ||||
-rw-r--r-- | Smarker/reflect.py | 395 | ||||
-rw-r--r-- | Smarker/requirements.txt | 10 | ||||
-rw-r--r-- | Smarker/temp.py | 6 | ||||
-rw-r--r-- | Smarker/templates/markdown.jinja2 | bin | 0 -> 26 bytes | |||
-rw-r--r-- | Smarker/templates/md.jinja2 | 166 | ||||
-rw-r--r-- | Smarker/templates/tex.jinja2 | 249 | ||||
-rw-r--r-- | Smarker/templates/text.jinja2 | bin | 0 -> 28 bytes | |||
-rw-r--r-- | Smarker/templates/txt.jinja2 | 168 |
14 files changed, 1440 insertions, 0 deletions
diff --git a/Smarker/database.py b/Smarker/database.py new file mode 100644 index 0000000..a0e3640 --- /dev/null +++ b/Smarker/database.py @@ -0,0 +1,97 @@ +from dataclasses import dataclass +import pymysql + +@dataclass +class SmarkerDatabase: + host:str + user:str + passwd:str + db:str + port:int = 3306 + + def __enter__(self): + try: + self.__connection = self.__get_connection() + except pymysql.err.OperationalError as e: + if e.args[0] == 1049: + self.__build_db() + return self + + def __exit__(self, type, value, traceback): + self.__connection.close() + + def __get_connection(self): + return pymysql.connect( + host = self.host, + port = self.port, + user = self.user, + passwd = self.passwd, + charset = "utf8mb4", + database = self.db + ) + + def __build_db(self): + self.__connection = pymysql.connect( + host = self.host, + port = self.port, + user = self.user, + passwd = self.passwd, + charset = "utf8mb4", + ) + with self.__connection.cursor() as cursor: + # unsafe: + cursor.execute("CREATE DATABASE %s" % self.db) + cursor.execute("USE %s" % self.db) + cursor.execute(""" + CREATE TABLE students( + student_no VARCHAR(10) PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + email VARCHAR(50) NOT NULL + ); + """) + cursor.execute(""" + CREATE TABLE assessment( + assessment_name VARCHAR(30) PRIMARY KEY NOT NULL, + yaml_path TEXT NOT NULL, + num_enrolled INT UNSIGNED NULL + ); + """) + cursor.execute(""" + CREATE TABLE submissions( + submission_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + student_no VARCHAR(10) NOT NULL, + assessment_name VARCHAR(30) NOT NULL, + submission_dt DATETIME NOT NULL default CURRENT_TIMESTAMP, + submission_zip_path TEXT NOT NULL, + FOREIGN KEY (student_no) REFERENCES students(student_no), + FOREIGN KEY (assessment_name) REFERENCES assessment(assessment_name) + ); + """) + cursor.execute(""" + CREATE TABLE assessment_file( + file_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + assessment_name VARCHAR(30) NOT NULL, + file_name VARCHAR(30) NOT NULL, + FOREIGN KEY (assessment_name) REFERENCES assessment(assessment_name) + ); + """) + cursor.execute(""" + CREATE TABLE submitted_files( + submission_id INT UNSIGNED NOT NULL, + file_id INT UNSIGNED NOT NULL, + file_text TEXT NOT NULL, + FOREIGN KEY (submission_id) REFERENCES submissions(submission_id), + FOREIGN KEY (file_id) REFERENCES assessment_file(file_id), + PRIMARY KEY(submission_id, file_id) + ); + """) + + + self.__connection.commit() + return self.__connection + + + def get_tables(self): + with self.__connection.cursor() as cursor: + cursor.execute("SHOW TABLES;") + return cursor.fetchall() diff --git a/Smarker/jinja_helpers.py b/Smarker/jinja_helpers.py new file mode 100644 index 0000000..6d2f34c --- /dev/null +++ b/Smarker/jinja_helpers.py @@ -0,0 +1,137 @@ +"""Functions in this module will be avaliable to call in jinja templates""" +import subprocess +import lxml.html +import datetime +import tempfile +import shutil +import pdfkit +import yaml +import json +import re +import os + +def get_datetime(): + return str(datetime.datetime.now()) + +def recurse_class_tree_text(tree, indent = 4): + return yaml.dump(tree, indent = indent).replace(": {}", "") + +def recurse_class_tree_forest(tree): + return re.sub(r"\"|:|\{\}|,", "", json.dumps(tree, indent=4)).replace("{", "[").replace("}", "]") + +def junit_xml_to_html(junit_xml, student_id): + # setup tempfiles for the junit xml and html + with tempfile.NamedTemporaryFile(suffix = ".xml", mode = "w", delete = False) as xml_f: + xml_f.write(junit_xml) + html_path = os.path.join(tempfile.mkdtemp(), "junit.html") + + # convert the junit xml to html + subprocess.run(["junit2html", xml_f.name, html_path]) + + # remove the html elements we don't like + root = lxml.html.parse(html_path) + for toremove in root.xpath("/html/body/h1"): + toremove.getparent().remove(toremove) + for toremove in root.xpath("/html/body/table"): + toremove.getparent().remove(toremove) + for toremove in root.xpath("/html/body/p"): + toremove.getparent().remove(toremove) + + # convert the html to pdf + out_fname = "%s_test_report.pdf" % student_id + pdfkit.from_string(lxml.etree.tostring(root).decode(), out_fname) + + # remove the tempfiles + # input("%s continue..." % html_path) + shutil.rmtree(os.path.split(html_path)[0]) + os.remove(xml_f.name) + + return out_fname + +def flatten_struct(struct): + # print("Attempting to flatten: ", struct) + out = {} + for s in struct: + key = list(s.keys())[0] + out[key] = s[key] + return out + +def get_required_num_args(funcname): + return int(re.findall(r"(?<=\()(\d+)(?=\))", funcname)[0]) + +def bool_to_yesno(b:bool): + if b: + return "YES" + else: + return "NO" + +def bool_to_checkbox(b:bool): + if b: + return "[x]" + else: + return "[ ]" + +def len_documentation(comments, docs): + """This function isn't in jinja""" + if comments == "None": + commentlen = 0 + else: + commentlen = len(comments) + + if docs == "None": + docslen = 0 + else: + docslen = len(docs) + + return commentlen + docslen + +def len_(obj): + return len(obj) + +def get_source_numlines(source): + return "%d lines (%d characters)" % (source.count("\n"), len(source)) + +#https://stackoverflow.com/questions/16259923/how-can-i-escape-latex-special-characters-inside-django-templates +def tex_escape(text): + conv = { + '&': r'\&', + '%': r'\%', + '$': r'\$', + '#': r'\#', + '_': r'\_', + '{': r'\{', + '}': r'\}', + '~': r'\textasciitilde{}', + '^': r'\^{}', + '\\': r'\textbackslash{}', + '<': r'\textless{}', + '>': r'\textgreater{}', + } + regex = re.compile('|'.join(re.escape(str(key)) for key in sorted(conv.keys(), key = lambda item: - len(item)))) + # print(text, regex.sub(lambda match: conv[match.group()], text)) + return regex.sub(lambda match: conv[match.group()], text) + +def get_python_latex_highlight_sty_path(): + return os.path.join(os.path.split(__file__)[0], "python-latex-highlighting", "pythonhighlight") + +def _get_helpers(): + import reflect + import os + + r = reflect.Reflect(os.path.split(__file__)[0]) + r.import_module("jinja_helpers") + return {k: v[0] for k, v in r.get_functions("jinja_helpers").items()} + +if __name__ == "__main__": + # import json + # with open("100301654_report.json", "r") as f: + # init_struct = json.load(f)["files"] + + # print(flatten_struct(flatten_struct(init_struct)["example.py"]["functions"])) + + print(get_python_latex_highlight_sty_path()) + + # print(_get_helpers()) + + +
\ No newline at end of file diff --git a/Smarker/mark.py b/Smarker/mark.py new file mode 100644 index 0000000..e8070f8 --- /dev/null +++ b/Smarker/mark.py @@ -0,0 +1,105 @@ +from dataclasses import dataclass +import jinja_helpers +import configparser +import misc_classes +import subprocess +import argparse +import tempfile +import zipfile +import reflect +import jinja2 +import shutil +import yaml +import json +import os + +def main(**kwargs): + student_no = os.path.splitext(os.path.split(args["submission"])[-1])[0] + + with misc_classes.ExtractZipToTempDir(args["submission"]) as submission_files: + with open(kwargs["assessment"], "r") as f: + assessment_struct = yaml.safe_load(f) + + with misc_classes.FileDependencies(assessment_struct): + output = reflect.gen_reflection_report(submission_files, assessment_struct, student_no, kwargs, args["submission"]) + output_file = kwargs["out"] + + if kwargs["format"] == "yaml": + strout = yaml.dump(output) + elif kwargs["format"] == "json": + strout = json.dumps(output, indent = 4) + else: + fp = os.path.join(os.path.split(__file__)[0], "templates", "%s.jinja2" % kwargs["format"]) + if kwargs["format"] in ("tex", "pdf"): + jinja_template = misc_classes.latex_jinja_env.get_template("tex.jinja2") + else: + with open(fp, "r") as f: + jinja_template = jinja2.Template(f.read()) + + strout = jinja_template.render(**output, **jinja_helpers._get_helpers(), **kwargs) + + if output_file == "stdout": + print(strout) + # input("\n\n[tempdir: %s] Press any key to close..." % tempdir) + exit() + + if output_file == "auto": + output_file = "%s_report.%s" % (student_no, kwargs["format"]) + + with open(output_file, "w") as f: + f.write(strout) + + if kwargs["format"] == "pdf": + os.environ["TEXINPUTS"] = os.path.join(os.path.split(__file__)[0], "python-latex-highlighting") + ":" + + os.rename(output_file, os.path.splitext(output_file)[0] + ".tex") + output_file = os.path.splitext(output_file)[0] + ".tex" + subprocess.run(["pdflatex", output_file]) + + os.remove(os.path.splitext(output_file)[0] + ".tex") + os.remove(os.path.splitext(output_file)[0] + ".log") + os.remove(os.path.splitext(output_file)[0] + ".aux") + + # input("\n\n[tempdir: %s] Press any key to close..." % tempdir) + +if __name__ == "__main__": + config = configparser.ConfigParser() + config.read(os.path.join(os.path.split(__file__)[0], "smarker.conf")) + + parser = argparse.ArgumentParser() + parser.add_argument( + "-a", "--assessment", + help = "Path to an assessment .yml file", + type = os.path.abspath, + required = True + ) + parser.add_argument( + "-s", "--submission", + help = "Path to a zip of a student's code", + type = os.path.abspath, + required = True + ) + parser.add_argument( + "-f", "--format", + help = "Output format type", + type = str, + choices = ["yaml", "json", "pdf"] + [os.path.splitext(f)[0] for f in os.listdir(os.path.join(os.path.split(__file__)[0], "templates"))], + default = "txt" + ) + parser.add_argument( + "-o", "--out", + help = "Path to write the output to, or, by default write to stdout. 'auto' automatically generates a file name.", + default = "stdout", + ) + + for section in config.sections(): + for option in config.options(section): + parser.add_argument( + "--%s_%s" % (section, option), + default = config.get(section, option), + help = "Optional argument inherited from config file. Read smarker.conf for details." + ) + + args = vars(parser.parse_args()) + main(**args) +
\ No newline at end of file diff --git a/Smarker/misc_classes.py b/Smarker/misc_classes.py new file mode 100644 index 0000000..09c3a7d --- /dev/null +++ b/Smarker/misc_classes.py @@ -0,0 +1,99 @@ +from dataclasses import dataclass
+import tempfile
+import zipfile
+import shutil
+import jinja2
+import os
+
+latex_jinja_env = jinja2.Environment(
+ block_start_string = '((*',
+ block_end_string = '*))',
+ variable_start_string = '(((',
+ variable_end_string = ')))',
+ comment_start_string = "((#",
+ comment_end_string = '#))',
+ line_statement_prefix = '%%',
+ line_comment_prefix = '%#',
+ trim_blocks = True,
+ autoescape = False,
+ loader = jinja2.FileSystemLoader(os.path.abspath(os.path.join(os.path.split(__file__)[0], "templates")))
+)
+
+@dataclass
+class ExtractZipToTempDir(tempfile.TemporaryDirectory):
+ zip_file:str
+
+ def __post_init__(self):
+ super().__init__()
+
+ def __enter__(self):
+ return self.extract()
+
+ def __exit__(self, exc, value, tb):
+ self.cleanup()
+
+ def extract(self):
+ with zipfile.ZipFile(self.zip_file) as z:
+ z.extractall(self.name)
+
+ # some zipping applications make a folder inside the zip with the files in that folder.
+ # try to deal with this here.
+ submission_files = self.name
+ if os.path.isdir(
+ os.path.join(submission_files, os.listdir(submission_files)[0])
+ ) and len(os.listdir(submission_files)) == 1:
+ submission_files = os.path.join(submission_files, os.listdir(submission_files)[0])
+
+ return submission_files
+
+@dataclass
+class FileDependencies:
+ assessment_struct:dict
+ to_:str=str(os.getcwd())
+
+ def __enter__(self):
+ self.get_deps()
+
+ def __exit__(self, type, value, traceback):
+ self.rm_deps()
+
+ def get_deps(self):
+ try:
+ for file_dep in self.assessment_struct["dependencies"]["files"]:
+ if os.path.isfile(file_dep):
+ shutil.copy(file_dep, os.path.join(self.to_, os.path.split(file_dep)[-1]))
+ else:
+ shutil.copytree(file_dep, os.path.join(self.to_, os.path.split(file_dep)[-1]))
+ except KeyError:
+ pass
+
+ def rm_deps(self):
+ stuff_to_remove = []
+ try:
+ stuff_to_remove += [os.path.split(f)[-1] for f in self.assessment_struct["dependencies"]["files"]]
+ except KeyError:
+ pass
+ try:
+ stuff_to_remove += self.assessment_struct["produced_files"]
+ except KeyError:
+ pass
+
+ for file_dep in stuff_to_remove:
+ file_dep = os.path.join(self.to_, file_dep)
+ # print("rm: ", file_dep)
+ if os.path.exists(file_dep):
+ if os.path.isfile(file_dep):
+ os.remove(file_dep)
+ else:
+ shutil.rmtree(file_dep)
+
+@dataclass
+class ChangeDirectory:
+ target:str
+ cwd:str=str(os.getcwd())
+
+ def __enter__(self):
+ os.chdir(self.target)
+
+ def __exit__(self, type, value, traceback):
+ os.chdir(self.cwd)
diff --git a/Smarker/pytest_template.jinja2 b/Smarker/pytest_template.jinja2 new file mode 100644 index 0000000..73c9a40 --- /dev/null +++ b/Smarker/pytest_template.jinja2 @@ -0,0 +1,8 @@ +{# generating python with python :3 #}
+
+import {{ module }}
+
+{% for i, test_code in enumerate(filestests, 1) %}
+def test_{{ i }}():
+ {{ test_code|indent(4, False) }} {# the code in the config file must be indented with 4 spaces only #}
+{% endfor %}
\ No newline at end of file diff --git a/Smarker/python-latex-highlighting b/Smarker/python-latex-highlighting new file mode 160000 +Subproject a5b8353876512d8d571a3c3be59452995318a17 diff --git a/Smarker/reflect.py b/Smarker/reflect.py new file mode 100644 index 0000000..bc5ae0e --- /dev/null +++ b/Smarker/reflect.py @@ -0,0 +1,395 @@ +import xml.etree.ElementTree as etree +from dataclasses import dataclass +from functools import reduce +from operator import getitem +import jinja_helpers +import misc_classes +import subprocess +import importlib +import traceback +import tempfile +import inspect +import pkgutil +import shutil +import jinja2 +import sys +import os +import re + +@dataclass +class Reflect: + client_code_path:str + imported_modules = {} + + def __post_init__(self): + self.client_code_path = os.path.normpath(self.client_code_path) + sys.path.insert(1, self.client_code_path) + self.client_modules = [p for p in pkgutil.iter_modules() if os.path.normpath(str(p[0])[12:-2]) == self.client_code_path] + # print("client moduules ", self.client_modules) + + def import_module(self, module_name): + """Imports a module. Before reflection can be conducted, a module + must be imported. WARNING: This will execute module code if it isn't + in a if __name__ == "__main__". Takes a module name (that the student made) + as the first argument. + + Args: + module_name (str): The name of a student's module to import + """ + for module in self.client_modules: + if module.name == module_name: + try: + self.imported_modules[module_name] = importlib.import_module(module.name) + except ModuleNotFoundError as e: + print("Missing library dependency for client module:") + print(e) + exit() + # except Exception as e: + # print("CRITICAL ERROR IN CLIENT CODE - CANNOT CONTINUE") + # raise ClientCodeException(e) + + def get_module_doc(self, module_name): + """Gets the documentation provided for a module. + + Args: + module_name (str): The student's module name to get documentation for + + Returns: + str: Provided documentation + """ + return { + "comments": self.__format_doc(inspect.getcomments(self.imported_modules[module_name])), + "doc": self.__format_doc(inspect.getdoc(self.imported_modules[module_name])) + } + + def get_classes(self, module_name): + """Gets the classes in a given module. The module must be imported first. + + Args: + module_name (str): The name of an imported module to get the name of. + + Returns: + dict: Dictionary of classes. The name of the class is the index, followed by + a tuple containing the class object and the classes' documentation. + """ + return { + i[0]: (i[1], {"comments": self.__format_doc(inspect.getcomments(i[1])), "doc": self.__format_doc(inspect.getdoc(i[1]))}) + for i in inspect.getmembers(self.imported_modules[module_name]) + if inspect.isclass(i[1]) and self.get_class_full_name(i[1]).split(".")[0] in self.imported_modules.keys() + } + + def get_class_methods(self, module_name, class_name): + """Gets the user generated methods of a given class. The module must be imported first. + + Args: + module_name (str): The name of the module in which the class is contained. + class_name (str): The name of the class. + + Returns: + dict: A dictionary of the methods. The index is the function name, followed by a tuple + containing the function object, the documentation, and the args as a nicely formatted string. + """ + return { + i[0]: ( + i[1], + {"comments": self.__format_doc(inspect.getcomments(i[1])), "doc": self.__format_doc(inspect.getdoc(i[1]))}, + str(inspect.signature(i[1])), + inspect.getsource(i[1]).rstrip() + ) + for i in inspect.getmembers( + self.get_classes(module_name)[class_name][0], + predicate=inspect.isfunction + ) + } + + def get_functions(self, module_name): + return { + i[0]: ( + i[1], + {"comments": self.__format_doc(inspect.getcomments(i[1])), "doc": self.__format_doc(inspect.getdoc(i[1]))}, + str(inspect.signature(i[1])), + inspect.getsource(i[1]).rstrip() + ) + for i in inspect.getmembers(self.imported_modules[module_name]) + if inspect.isfunction(i[1]) + } + + def get_class_full_name(self, class_): + """Returns the name of a class object as a nice string. e.g. modulename.classname + except if it's a builtin there'll be no module name. + + Args: + class_ (class): A class to get the name of + + Returns: + str: A nicely formatted class name. + """ + if class_.__module__ in ['builtins', 'exceptions']: + return class_.__name__ + return "%s.%s" % (class_.__module__, class_.__name__) + + # classes that inherit from two classes doesn't print out nicely here. + # using this method is better https://pastebin.com/YuxkkTkv + def get_class_tree(self): + """Generates a dictionary based tree structure showing inheritance of classes + of all the *imported modules*. WARNING: It doesn't deal well with multiple inheritance.. + Read the comments. + """ + + def expand(a:list): + out = [] + for l in a: + for i in reversed(range(0, len(l))): + out.append(l[:len(l) - i]) + return out + + # https://www.geeksforgeeks.org/python-convert-a-list-of-lists-into-tree-like-dict/ + def getTree(tree, mappings): + return reduce(getitem, mappings, tree) + + # https://www.geeksforgeeks.org/python-convert-a-list-of-lists-into-tree-like-dict/ + def setTree(tree, mappings): + getTree(tree, mappings[:-1])[mappings[-1]] = dict() + + unexpanded_class_paths = [] + for module in self.imported_modules.keys(): + for class_ in self.get_classes(module).values(): + unexpanded_class_paths.append([ + self.get_class_full_name(i) + for i in reversed(list(inspect.getmro(class_[0]))) + ]) + + tree = {} + added = [] # the expander makes duplicates. keep a list to remove them + # sadly a collections.Counter doesnt work with lists of lists + for s in expand(unexpanded_class_paths): + if s not in added: + setTree(tree, [i for i in reversed(s)][::-1]) + added.append(s) + + # print(tree) + # return inspect.getclasstree(classes) + return tree + + def run_tests(self, tests, run_colourful = False): + """Build and run pytests from the configuration yaml. Indentation needs to + be four spaces only otherwise it won't work. We recommend running this last + so all modules are already imported. + + Args: + tests (dict): dict with the filename as the key followed by a list of + python code to make the test + run_colourful (bool, optional): Run pytest again, printing out the + exact output of pytest as soon as it's ready. Has the advantage that + colours are preserved, but is only useful for when the user wants to + print out the report to stdout. Defaults to False. + + Returns: + [dict]: A dictionary consisting of the pytest output string, junit xml + output (which might be useful for rendering nicely in some output formats) + and some nice meta information. + """ + test_results = {} + test_results["pytest_report"] = "" + + with open(os.path.join(os.path.split(__file__)[0], "pytest_template.jinja2"), "r") as f: + jinja_template = jinja2.Template(f.read()) + + for filename, filestests in tests.items(): + with open(os.path.join(self.client_code_path, "test_" + filename), "w") as f: + f.write(jinja_template.render( + module = os.path.splitext(filename)[0], + filestests = filestests, + enumerate = enumerate # a function thats needed + )) + + with tempfile.TemporaryDirectory() as tmp: + junitxmlpath = os.path.join(tmp, "report.xml") + test_files = [os.path.join(self.client_code_path, "test_%s" % f) for f in tests.keys()] + cmd = ["pytest", "-vv"] + test_files + ["--junitxml=%s" % junitxmlpath] + # print("cmd: ", " ".join(cmd)) + if test_files == []: + test_results["pytest_report"] = "*** No Tests ***" + return test_results + proc = subprocess.Popen(cmd, stdout = subprocess.PIPE) + while True: + line = proc.stdout.readline() + if not line: + break + test_results["pytest_report"] += line.decode() + + with open(junitxmlpath, "r") as f: + test_results["junitxml"] = f.read() + root = etree.fromstring(test_results["junitxml"]) + test_results["meta"] = root.findall("./testsuite")[0].attrib + + if run_colourful: + subprocess.run(cmd) + + return test_results + + def __format_doc(*doc): + return str(doc[1]).rstrip() + +def gen_reflection_report(client_code_path, assessment_struct, student_no, configuration, zip_file): + reflection = Reflect(client_code_path) + present_module_names = [i.name for i in reflection.client_modules] + out = assessment_struct + out["student_no"] = student_no + tests_to_run = {} + + try: + produced_files = assessment_struct["produced_files"] + except KeyError: + produced_files = [] + + for i, required_file in enumerate(assessment_struct["files"], 0): + required_file = list(required_file.keys())[0] + module_name = os.path.splitext(required_file)[0] + + if module_name in present_module_names: + out["files"][i][required_file]["present"] = True + else: + out["files"][i][required_file]["present"] = False + continue + + try: + reflection.import_module(module_name) + except Exception as e: + out["files"][i][required_file]["has_exception"] = True + out["files"][i][required_file]["exception"] = {} + out["files"][i][required_file]["exception"]["type"] = str(type(e)) + out["files"][i][required_file]["exception"]["str"] = str(e) + # TODO: test this indexing so we can be sure we're getting client code only + e_list = traceback.format_exception(None, e, e.__traceback__) + out["files"][i][required_file]["exception"]["traceback"] = ''.join([e_list[0]] + e_list[12:]) + + continue + + required_files_features = assessment_struct["files"][i][required_file] + out["files"][i][required_file]["has_exception"] = False + out["files"][i][required_file]["documentation"] = reflection.get_module_doc(module_name) + if "classes" in required_files_features.keys(): + + present_classes = reflection.get_classes(module_name) + for j, class_name in enumerate(required_files_features["classes"], 0): + class_name = list(class_name.keys())[0] + + stop_here_flag = False + # surprised the yaml parser doesnt do this automatically... + if out["files"][i][required_file]["classes"][j][class_name] is None: + out["files"][i][required_file]["classes"][j][class_name] = {} + stop_here_flag = True + + if class_name in present_classes.keys(): + out["files"][i][required_file]["classes"][j][class_name]["present"] = True + else: + out["files"][i][required_file]["classes"][j][class_name]["present"] = False + continue + + # print( present_classes[class_name][1]) + out["files"][i][required_file]["classes"][j][class_name]["documentation"] = present_classes[class_name][1] + + if stop_here_flag: + continue + + present_methods = reflection.get_class_methods(module_name, class_name) + + for k, required_method in enumerate(assessment_struct["files"][i][required_file]["classes"][j][class_name]["methods"], 0): + out["files"][i][required_file]["classes"][j][class_name]["methods"][k] = {required_method: {}} + + method_name = re.sub(r"\(\d+\)", "", required_method) + if method_name in present_methods.keys(): + out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["present"] = True + else: + out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["present"] = False + continue + + out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["arguments"] = present_methods[method_name][-2] + out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["minimum_arguments"] = present_methods[method_name][-2].count(",") + 1 + out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["documentation"] = present_methods[method_name][-3] + out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["source_code"] = present_methods[method_name][-1] + + if "functions" in required_files_features.keys(): + present_functions = reflection.get_functions(module_name) + for j, required_function in enumerate(assessment_struct["files"][i][required_file]["functions"], 0): + function_name = re.sub(r"\(\d+\)", "", required_function) + out["files"][i][required_file]["functions"][j] = {required_function: {}} + + if function_name in present_functions.keys(): + out["files"][i][required_file]["functions"][j][required_function]["present"] = True + else: + out["files"][i][required_file]["functions"][j][required_function]["present"] = False + continue + + out["files"][i][required_file]["functions"][j][required_function]["documentation"] = present_functions[function_name][-3] + out["files"][i][required_file]["functions"][j][required_function]["arguments"] = present_functions[function_name][-2] + out["files"][i][required_file]["functions"][j][required_function]["minimum_arguments"] = present_functions[function_name][-2].count(",") + 1 + out["files"][i][required_file]["functions"][j][required_function]["source_code"] = present_functions[function_name][-1] + + if "tests" in required_files_features.keys(): + filename = list(assessment_struct["files"][i].keys())[0] + if not out["files"][i][filename]["has_exception"]: + for j, test in enumerate(assessment_struct["files"][i][required_file]["tests"], 0): + try: + tests_to_run[filename].append(test) + except KeyError: + tests_to_run[filename] = [test] + + if "run" in required_files_features.keys(): + filename = list(assessment_struct["files"][i].keys())[0] + with misc_classes.ExtractZipToTempDir(zip_file) as tempdir: + with misc_classes.FileDependencies(assessment_struct, tempdir): + with misc_classes.ChangeDirectory(tempdir): + for j, runtime in enumerate(assessment_struct["files"][i][required_file]["run"], 0): + for cmd, contents in runtime.items(): + lines = "" + if "monitor" in contents.keys(): + + if contents["monitor"] not in produced_files: + raise MonitoredFileNotInProducedFilesException("The monitored file %s is not in the list of produced files. It needs to be added." % contents["monitor"]) + + subprocess.run(cmd.split()) + if os.path.exists(contents["monitor"]): + with open(contents["monitor"], "r") as f: + lines = f.read() + else: + lines = "*** File not produced ***" + # yes, this could potentially cause regexes to still be found + + else: + proc = subprocess.Popen(cmd.split(), stdout = subprocess.PIPE) + while True: + line = proc.stdout.readline() + if not line: + break + lines += line.decode() + + lines = lines.replace("\r", "") + matches = {} + for regex_ in contents["regexes"]: + matches[regex_] = re.findall(regex_, lines) + required_files_features["run"][j][cmd]["regexes"] = matches + required_files_features["run"][j][cmd]["full_output"] = lines + + out["test_results"] = reflection.run_tests(tests_to_run, configuration["out"] == "stdout" and configuration["format"] in ["text", "txt"]) + out["class_tree"] = reflection.get_class_tree() + return out + +class MonitoredFileNotInProducedFilesException(Exception): + pass + +if __name__ == "__main__": + # user_code_path = "D:\\Edencloud\\UniStuff\\3.0 - CMP 3rd Year Project\\Smarker\\../ExampleSubmissions/Submission_A" + + # reflect = Reflect(user_code_path) + # reflect.import_module("pjtool") + # # for c, v in reflect.get_classes(("pjtool")).items(): + # # print(c, v) + # for k, v in reflect.get_functions("pjtool").items(): + # print(k, v) + + reflect = Reflect(os.getcwd()) + print(reflect.client_modules) + reflect.import_module("jinja_helpers") + print({k: v for k, v in reflect.get_functions("jinja_helpers").items()})
\ No newline at end of file diff --git a/Smarker/requirements.txt b/Smarker/requirements.txt new file mode 100644 index 0000000..3831b5e --- /dev/null +++ b/Smarker/requirements.txt @@ -0,0 +1,10 @@ +# sudo apt-get install wkhtmltopdf
+# https://github.com/olivierverdier/python-latex-highlighting
+Jinja2==3.0.3
+misaka==2.1.1
+Pygments==2.10.0
+PyYAML==6.0
+pytest
+junit2html
+pdfkit
+lxml
diff --git a/Smarker/temp.py b/Smarker/temp.py new file mode 100644 index 0000000..60b1c18 --- /dev/null +++ b/Smarker/temp.py @@ -0,0 +1,6 @@ +import json + +with open("100301654_report.json", "r") as f: + tree = json.load(f)["class_tree"] + +print(tree)
\ No newline at end of file diff --git a/Smarker/templates/markdown.jinja2 b/Smarker/templates/markdown.jinja2 Binary files differnew file mode 100644 index 0000000..99c26ce --- /dev/null +++ b/Smarker/templates/markdown.jinja2 diff --git a/Smarker/templates/md.jinja2 b/Smarker/templates/md.jinja2 new file mode 100644 index 0000000..e764a49 --- /dev/null +++ b/Smarker/templates/md.jinja2 @@ -0,0 +1,166 @@ +{%- macro expand_function(function_name, function_contents, x = "Function") -%}
+ - `{{ function_name }}`:
+{%- if function_contents["present"] %}
+ - **Arguments:**
+ - `{{ function_contents["arguments"] }}`
+ - {{ bool_to_checkbox(function_contents["minimum_arguments"] >= get_required_num_args(function_name)) }} Enough?
+ - **Documentation**:
+ - {{ len_documentation(function_contents["documentation"]["comments"], function_contents["documentation"]["doc"]) }} characters long
+{%- if md_show_full_docs == "True" %}
+ - Comments:
+ {%- if function_contents["documentation"]["comments"] == "None" %}
+ - [ ] No comments present
+{%- else %}
+{{ code_block(function_contents["documentation"]["comments"])|indent(12, True) }}
+{%- endif %}
+ - Docstring:
+{%- if function_contents["documentation"]["doc"] == "None" %}
+ - [ ] No docstring present
+{%- else %}
+{{ code_block(function_contents["documentation"]["doc"])|indent(12, True) }}
+{%- endif -%}
+{%- endif %}
+ - **Source**:
+ - {{ get_source_numlines(function_contents["source_code"]) }}
+{%- if md_show_source == "True" %}
+ - Code:
+{{ code_block(function_contents["source_code"])|indent(12, True) }}
+{%- endif %}
+{%- else %}
+ - [ ] {{ x }} not present
+{%- endif %}
+{%- endmacro -%}
+
+{%- macro code_block(code) -%}
+```
+{{ code }}
+```
+{%- endmacro -%}
+
+# {{ name }} - Student ID: {{ student_no }} Automatic marking report
+Report generated at {{ get_datetime() }}
+## Class Tree:
+
+```
+{{ recurse_class_tree_text(class_tree) }}
+```
+
+## File Analysis
+
+{%- set flat_files = flatten_struct(files) %}
+{% for filename, files_contents in flat_files.items() %}
+### File `{{ filename }}`:
+{%- if files_contents["present"] -%}
+{%- if files_contents["has_exception"] %}
+*** File cannot be run - has compile time exception ***
+Please note that this file cannot be analysed or have tests preformed upon it-
+ this can lead to the whole test suite failing if another module imports this.
+ - Exception Type: `{{ files_contents["exception"]["type"] }}`
+ - Exception String: `{{ files_contents["exception"]["str"] }}`
+ - Full Traceback:
+```
+{{ files_contents["exception"]["traceback"] }}
+```
+{%- else %}
+ - #### Documentation:
+ {%- set len_docs = len_documentation(files_contents["documentation"]["comments"], files_contents["documentation"]["doc"]) %}
+ - {{ len_docs }} characters long
+{%- if md_show_full_docs == "True" %}
+ - ##### Comments:
+{%- if files_contents["documentation"]["comments"] == "None" %}
+ - [ ] No comments present
+{%- else %}
+{{ code_block(files_contents["documentation"]["comments"])|indent(8, True) }}
+{%- endif %}
+ - ##### Docstring:
+{%- if files_contents["documentation"]["doc"] == "None" %}
+ - [ ] No docstring present
+{%- else %}
+{{ code_block(files_contents["documentation"]["doc"])|indent(8, True) }}
+{%- endif -%}
+{%- endif %}
+{%- if "classes" in files_contents.keys() %}
+ - #### Classes:
+{%- set flat_classes = flatten_struct(files_contents["classes"]) -%}
+{% for class_name, class_contents in flat_classes.items() %}
+ - ##### `{{ class_name}}`:
+{%- if class_contents["present"] %}
+ - ###### Documentation:
+ {%- set len_docs = len_documentation(class_contents["documentation"]["comments"], class_contents["documentation"]["doc"]) %}
+ - {{ len_docs }} characters long
+{%- if md_show_full_docs == "True" %}
+ - *Comments*:
+{%- if class_contents["documentation"]["comments"] == "None" %}
+ - [ ] No comments present
+{%- else %}
+{{ code_block(class_contents["documentation"]["comments"])|indent(20, True) }}
+{%- endif %}
+ - *Docstring*:
+{%- if class_contents["documentation"]["doc"] == "None" %}
+ - [ ] No docstring present
+{%- else %}
+{{ code_block(class_contents["documentation"]["doc"])|indent(20, True) }}
+{%- endif -%}
+{%- endif %}
+{%- if "methods" in class_contents.keys() %}
+ - ###### Methods:
+{%- set flat_methods = flatten_struct(class_contents["methods"]) -%}
+{%- for method_name, method_contents in flat_methods.items() %}
+{{ expand_function(method_name, method_contents, "Method")|indent(16, True) }}
+{%- endfor -%}
+{%- endif -%}
+{%- else %}
+ - [ ] Class not present
+{%- endif -%}
+{%- endfor -%}
+{%- endif -%}
+{% if "functions" in files_contents.keys() %}
+ - #### Functions:
+{%- set flat_functions = flatten_struct(files_contents["functions"]) %}
+{%- for function_name, function_contents in flat_functions.items() %}
+{{ expand_function(function_name, function_contents)|indent(8, True) }}
+{%- endfor -%}
+{%- endif -%}
+{% if "run" in files_contents.keys() %}
+ - #### Runtime Analysis:
+{%- set flat_runtime = flatten_struct(files_contents["run"]) %}
+{%- for cmd, runtime_contents in flat_runtime.items() %}
+ - ##### Command `{{ cmd }}`:
+ - **Monitor:**
+{%- if "monitor" in runtime_contents.keys() %}
+ - {{ runtime_contents["monitor"] }}
+{%- else %}
+ - stdout
+{%- endif %}
+ - **Regexes:**
+{%- for regex_, results in runtime_contents["regexes"].items() %}
+ - `{{regex_}}`:
+ - Found occurrences: {{ len_(results) }}
+{%- if code_block(runtime_contents["full_output"]) == "*** File not produced ***" %}
+ - *** File was not produced- no occurrences ***
+{%- endif -%}
+{%- if md_show_all_regex_occurrences == "True" and len_(results) > 0 %}
+ - Occurrences list:
+{%- for result in results %}
+ - `{{ result.replace("\n", "\\n") }}`
+{%- endfor -%}
+{%- if md_show_all_run_output == "True" %}
+ - Full runtime output:
+{{ code_block(runtime_contents["full_output"])|indent(24, True) }}
+{%- endif -%}
+{%- endif -%}
+{%- endfor -%}
+{%- endfor -%}
+{%- endif -%}
+{%- endif -%}
+{% else %}
+ - [ ] File not present
+{% endif %}
+{% endfor %}
+
+{% if out != "stdout" and format != "html" -%}
+## Tests:
+```
+{{ test_results["pytest_report"].replace("\r", "") }}
+```
+{%- endif -%}
\ No newline at end of file diff --git a/Smarker/templates/tex.jinja2 b/Smarker/templates/tex.jinja2 new file mode 100644 index 0000000..eaa7db7 --- /dev/null +++ b/Smarker/templates/tex.jinja2 @@ -0,0 +1,249 @@ +((* macro expand_function(function_name, function_contents, x = "Function") *))
+ \texttt{((( tex_escape(function_name) )))}:
+
+ ((* if function_contents["present"] *))
+ \begin{itemize}
+ \item Arguments: \pyth{((( function_contents["arguments"] )))}
+ \item Documentation: ((( len_documentation(function_contents["documentation"]["comments"], function_contents["documentation"]["doc"]) ))) characters long
+ ((* if tex_show_full_docs == "True" *))
+
+ \textbf{Comments:}
+ ((*- if function_contents["documentation"]["comments"] == "None" *))
+ \errortext{No comments present.}
+ ((* else *))
+ \begin{lstlisting}
+((( function_contents["documentation"]["comments"] )))
+ \end{lstlisting}
+ ((* endif *))
+
+ \textbf{Docstring}:
+ ((*- if function_contents["documentation"]["doc"] == "None" *))
+ \errortext{No docstring present.}
+ ((* else *))
+ \begin{lstlisting}
+((( function_contents["documentation"]["doc"] )))
+ \end{lstlisting}
+ ((* endif *))
+ ((* endif *))
+ \item Code: ((( get_source_numlines(function_contents["source_code"]) )))
+ ((* if tex_show_source == "True" *))
+ \begin{python}
+((( function_contents["source_code"] )))
+ \end{python}
+ ((* endif *))
+ \end{itemize}
+ ((* else *))
+ \errortext{((( x ))) \texttt{((( tex_escape(function_name) )))} not present.}
+ ((* endif *))
+((* endmacro *))
+
+\documentclass{article}
+
+\usepackage{pythonhighlight}
+
+\usepackage[margin=1in]{geometry} % margins
+\usepackage{multicol} % columns
+\usepackage{float} % layout
+\usepackage{forest} % for the class tree
+\usepackage{pdfpages} % for importing the test results pdf
+\usepackage{xcolor} % colours
+\usepackage{listings}
+\lstset{
+basicstyle=\small\ttfamily,
+columns=flexible,
+breaklines=true
+}
+
+\newcommand{\errortext}[1]{\textcolor{red}{\textbf{#1}}}
+
+\author{((( student_no )))}
+\title{((( name ))) - Automatic marking report}
+
+\begin{document}
+
+((* if tex_columns != "1" *))
+\begin{multicols}{((( tex_columns )))}
+((* endif *))
+
+\maketitle
+\section{Class Tree}
+
+\begin{figure}[H]
+ \centering
+ \begin{forest}
+ ((( recurse_class_tree_forest(class_tree)|indent(8, False) )))
+ \end{forest}
+ \caption{Class inheritance tree}
+\end{figure}
+
+\section{File Analysis}
+((* set flat_files = flatten_struct(files) *))
+((* for filename, files_contents in flat_files.items() *))
+ \subsection{\texttt{((( filename )))}}
+ ((* if files_contents["present"] *))
+ ((* if files_contents["has_exception"] *))
+ \errortext{File cannot be run - has compile time exception.}
+
+ Please note that this file cannot be analysed or have tests preformed upon it-
+ this can lead to the whole test suite failing if another module imports this.
+
+ \textbf{Exception Type:} \texttt{((( files_contents["exception"]["type"] )))}
+
+ \textbf{Exception String:} \texttt{((( files_contents["exception"]["str"] )))}
+
+ \textbf{Full Traceback:}
+
+ \begin{lstlisting}
+((( files_contents["exception"]["traceback"] )))
+ \end{lstlisting}
+ ((* else *))
+ \begin{itemize}
+ \item \textbf{Documentation:}
+
+ ((( len_documentation(files_contents["documentation"]["comments"], files_contents["documentation"]["doc"]) ))) characters long
+ ((* if tex_show_full_docs == "True" *))
+
+ \item \textbf{Comments:}
+ ((*- if files_contents["documentation"]["comments"] == "None" *))
+ \errortext{No comments present.}
+ ((* else *))
+ \begin{lstlisting}
+((( files_contents["documentation"]["comments"] )))
+ \end{lstlisting}
+ ((* endif *))
+
+ \item \textbf{Docstring:}
+ ((*- if files_contents["documentation"]["doc"] == "None" *))
+ \errortext{No docstring present.}
+ ((* else *))
+ \begin{lstlisting}
+((( files_contents["documentation"]["doc"] )))
+ \end{lstlisting}
+ ((* endif *))
+
+ ((* endif *))
+ \end{itemize}
+
+ ((* if "classes" in files_contents.keys() *))
+ \subsubsection{Classes}
+
+ ((* set flat_classes = flatten_struct(files_contents["classes"]) *))
+ ((* for class_name, class_contents in flat_classes.items() *))
+ \begin{itemize}
+
+
+ \item \texttt{((( class_name )))}:
+
+ ((* if class_contents["present"] *))
+ \begin{itemize}
+ \item \textbf{Documentation:}
+ ((( len_documentation(class_contents["documentation"]["comments"], class_contents["documentation"]["doc"]) ))) characters long
+
+ ((* if tex_show_full_docs == "True" *))
+
+
+ \item \textbf{Comments:}
+
+ ((* if class_contents["documentation"]["comments"] == "None" -*))
+ \errortext{No comments present.}
+ ((* else *))
+ \begin{lstlisting}
+((( class_contents["documentation"]["comments"] )))
+ \end{lstlisting}
+ ((* endif *))
+
+
+ \item \textbf{Docstring:}
+
+ ((* if class_contents["documentation"]["doc"] == "None" -*))
+ \errortext{No docstring present.}
+ ((* else *))
+ \begin{lstlisting}
+((( class_contents["documentation"]["doc"] )))
+ \end{lstlisting}
+ ((* endif *))
+
+ ((* if "methods" in class_contents.keys() *))
+ \item \textbf{Methods:}
+ ((* set flat_methods = flatten_struct(class_contents["methods"]) *))
+ \begin{itemize}
+ ((* for method_name, method_contents in flat_methods.items() *))
+ \item ((( expand_function(method_name, method_contents, x = "Method") )))
+ ((* endfor *))
+ \end{itemize}
+
+ ((* endif *))
+ \end{itemize}
+ ((* endif *))
+
+ ((* else *))
+
+ \errortext{Class not present.}
+
+ ((* endif *))
+
+ \end{itemize}
+ ((* endfor *))
+
+
+ ((* endif *))
+
+ ((* if "functions" in files_contents.keys() *))
+ \subsubsection{Functions}
+ ((* set flat_functions = flatten_struct(files_contents["functions"]) *))
+ \begin{itemize}
+ ((* for function_name, function_contents in flat_functions.items() *))
+ \item ((( expand_function(function_name, function_contents) )))
+ ((* endfor *))
+ \end{itemize}
+ ((* endif *))
+
+ \subsubsection{Runtime Analysis}
+ ((* set flat_runtime = flatten_struct(files_contents["run"]) *))
+ \begin{itemize}
+ ((* for cmd, runtime_contents in flat_runtime.items() *))
+ \item Command: \texttt{((( tex_escape(cmd) )))}
+ \item Monitor:
+ ((*- if "monitor" in runtime_contents.keys() *))
+ \texttt{((( tex_escape(runtime_contents["monitor"]) )))}
+ ((*- else *))
+ stdout
+ ((*- endif *))
+ \item Regexes:
+ ((* for regex_, results in runtime_contents["regexes"].items() *))
+ \begin{itemize}
+ \item \texttt{((( tex_escape(regex_) )))}:
+ \begin{itemize}
+ \item Found occurrences: ((( len_(results) )))
+ ((* if txt_show_all_regex_occurrences == "True" and len_(results) > 0 *))
+ \item Occurences list:
+ \begin{enumerate}
+ ((* for result in results *))
+ \item \texttt{((( tex_escape(result.replace("\n", "\\n")) )))}
+ ((* endfor *))
+ \end{enumerate}
+ ((* endif *))
+ \end{itemize}
+ \end{itemize}
+ ((*- endfor -*))
+ ((* endfor *))
+ \end{itemize}
+
+ ((* endif *))
+ ((* else *))
+ \errortext{File is not present.}
+ ((* endif *))
+((* endfor *))
+
+\section{Tests}
+((* if test_results["pytest_report"] == "*** No Tests ***" *))
+ No tests were executed.
+((* else *))
+ \includepdf[pages={1-},scale=1.0]{((( junit_xml_to_html(test_results["junitxml"], student_no) )))}
+((* endif *))
+
+((* if tex_columns != "1" *))
+\end{multicols}
+((* endif *))
+
+\end{document}
\ No newline at end of file diff --git a/Smarker/templates/text.jinja2 b/Smarker/templates/text.jinja2 Binary files differnew file mode 100644 index 0000000..eca6ebd --- /dev/null +++ b/Smarker/templates/text.jinja2 diff --git a/Smarker/templates/txt.jinja2 b/Smarker/templates/txt.jinja2 new file mode 100644 index 0000000..9eb4beb --- /dev/null +++ b/Smarker/templates/txt.jinja2 @@ -0,0 +1,168 @@ +{%- macro expand_function(function_name, function_contents, x = "Function") -%} +{{ function_name + ":" }} +{%- if function_contents["present"] %} + Arguments: + {{ function_contents["arguments"] }} + Enough? {{ bool_to_yesno(function_contents["minimum_arguments"] >= get_required_num_args(function_name)) }} + Documentation: + {{ len_documentation(function_contents["documentation"]["comments"], function_contents["documentation"]["doc"]) }} characters long + {%- if txt_show_full_docs == "True" %} + Comments: + {%- if function_contents["documentation"]["comments"] == "None" %} + *** No comments present *** + {%- else %} +``` +{{ function_contents["documentation"]["comments"] }} +``` + {%- endif %} + Docstring: + {%- if function_contents["documentation"]["doc"] == "None" %} + *** No docstring present *** + {%- else %} +``` +{{ function_contents["documentation"]["doc"] }} +``` + {%- endif -%} + {%- endif %} + Source: + {{ get_source_numlines(function_contents["source_code"]) }} + {%- if txt_show_source == "True" %} + Code: +``` +{{ function_contents["source_code"] }} +``` + {%- endif %} +{%- else %} + *** {{ x }} not present *** +{%- endif %} +{%- endmacro -%} + +=== {{ name }} - Student ID: {{ student_no }} Automatic marking report === +Report generated at {{ get_datetime() }} + +== Class Tree: == + +{{ recurse_class_tree_text(class_tree) }} + +== File Analysis == +{%- set flat_files = flatten_struct(files) %} +{% for filename, files_contents in flat_files.items() %} + = {{ filename + " =" -}} + {%- if files_contents["present"] -%} + {%- if files_contents["has_exception"] %} + *** File cannot be run - has compile time exception *** + Please note that this file cannot be analysed or have tests preformed upon it- + this can lead to the whole test suite failing if another module imports this. + Exception Type: + {{ files_contents["exception"]["type"] }} + Exception String: + {{ files_contents["exception"]["str"] }} + Full Traceback: +``` +{{ files_contents["exception"]["traceback"] }} +``` + {%- else %} + Documentation: + {{ len_documentation(files_contents["documentation"]["comments"], files_contents["documentation"]["doc"]) }} characters long + {%- if txt_show_full_docs == "True" %} + Comments: + {%- if files_contents["documentation"]["comments"] == "None" %} + *** No comments present *** + {%- else %} + ``` + {{ files_contents["documentation"]["comments"]|indent(16, False) }} + ``` + {%- endif %} + Docstring: + {%- if files_contents["documentation"]["doc"] == "None" %} + *** No docstring present *** + {%- else %} + ``` + {{ files_contents["documentation"]["doc"]|indent(16, False) }} + ``` + {%- endif -%} + {%- endif %} + {%- if "classes" in files_contents.keys() %} + Classes: + {%- set flat_classes = flatten_struct(files_contents["classes"]) -%} + {% for class_name, class_contents in flat_classes.items() %} + {{ class_name + ":" }} + {%- if class_contents["present"] %} + Documentation: + {{ len_documentation(class_contents["documentation"]["comments"], class_contents["documentation"]["doc"]) }} characters long + {%- if txt_show_full_docs == "True" %} + Comments: + {%- if class_contents["documentation"]["comments"] == "None" %} + *** No comments present *** + {%- else %} + ``` + {{ class_contents["documentation"]["comments"]|indent(16, False) }} + ``` + {%- endif %} + Docstring: + {%- if class_contents["documentation"]["doc"] == "None" %} + *** No docstring present *** + {%- else %} + ``` + {{ class_contents["documentation"]["doc"]|indent(16, False) }} + ``` + {%- endif -%} + {%- endif %} + {%- if "methods" in class_contents.keys() %} + Methods: + {%- set flat_methods = flatten_struct(class_contents["methods"]) -%} + {%- for method_name, method_contents in flat_methods.items() %} + {{ expand_function(method_name, method_contents, "Method")|indent(20, False) }} + {%- endfor -%} + {%- endif -%} + {%- else %} + *** Class not present *** + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {% if "functions" in files_contents.keys() %} + Functions: + {%- set flat_functions = flatten_struct(files_contents["functions"]) %} + {%- for function_name, function_contents in flat_functions.items() %} + {{ expand_function(function_name, function_contents)|indent(12, False) }} + {%- endfor -%} + {%- endif -%} + {% if "run" in files_contents.keys() %} + Runtime Analysis: + {%- set flat_runtime = flatten_struct(files_contents["run"]) %} + {%- for cmd, runtime_contents in flat_runtime.items() %} + Command `{{ cmd }}`: + Monitor: + {%- if "monitor" in runtime_contents.keys() %} + {{ runtime_contents["monitor"] }} + {%- else %} + stdout + {%- endif %} + Regexes: + {%- for regex_, results in runtime_contents["regexes"].items() %} + `{{regex_}}`: + Found occurrences: {{ len_(results) }} + {%- if txt_show_all_regex_occurrences == "True" and len_(results) > 0 %} + Occurrences list: + {%- for result in results %} + {{ result.replace("\n", "\\n") }} + {%- endfor -%} + {%- endif -%} + {%- endfor -%} + {%- if txt_show_all_run_output == "True" %} + Full runtime output: + ``` + {{ runtime_contents["full_output"]|indent(20, False) }} + ``` + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- endif -%} + {% else %} + *** File not present *** + {% endif %} +{% endfor %} + +{% if out != "stdout" -%} +{{ test_results["pytest_report"].replace("\r", "") }} +{%- endif -%}
\ No newline at end of file |