From 12ac9bd354c664978523547ffac9bbebf0fcb577 Mon Sep 17 00:00:00 2001 From: jwansek Date: Fri, 22 Apr 2022 16:02:28 +0100 Subject: Moved and adapted to move folder, added rendering to pdfs --- Smarker/database.py | 97 ++++++++++ Smarker/jinja_helpers.py | 137 +++++++++++++ Smarker/mark.py | 105 ++++++++++ Smarker/misc_classes.py | 99 ++++++++++ Smarker/pytest_template.jinja2 | 8 + Smarker/python-latex-highlighting | 1 + Smarker/reflect.py | 395 ++++++++++++++++++++++++++++++++++++++ Smarker/requirements.txt | 10 + Smarker/temp.py | 6 + Smarker/templates/markdown.jinja2 | Bin 0 -> 26 bytes Smarker/templates/md.jinja2 | 166 ++++++++++++++++ Smarker/templates/tex.jinja2 | 249 ++++++++++++++++++++++++ Smarker/templates/text.jinja2 | Bin 0 -> 28 bytes Smarker/templates/txt.jinja2 | 168 ++++++++++++++++ 14 files changed, 1441 insertions(+) create mode 100644 Smarker/database.py create mode 100644 Smarker/jinja_helpers.py create mode 100644 Smarker/mark.py create mode 100644 Smarker/misc_classes.py create mode 100644 Smarker/pytest_template.jinja2 create mode 160000 Smarker/python-latex-highlighting create mode 100644 Smarker/reflect.py create mode 100644 Smarker/requirements.txt create mode 100644 Smarker/temp.py create mode 100644 Smarker/templates/markdown.jinja2 create mode 100644 Smarker/templates/md.jinja2 create mode 100644 Smarker/templates/tex.jinja2 create mode 100644 Smarker/templates/text.jinja2 create mode 100644 Smarker/templates/txt.jinja2 (limited to 'Smarker') 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 index 0000000..a5b8353 --- /dev/null +++ b/Smarker/python-latex-highlighting @@ -0,0 +1 @@ +Subproject commit a5b8353876512d8d571a3c3be59452995318a177 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 new file mode 100644 index 0000000..99c26ce Binary files /dev/null and b/Smarker/templates/markdown.jinja2 differ 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 new file mode 100644 index 0000000..eca6ebd Binary files /dev/null and b/Smarker/templates/text.jinja2 differ 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 -- cgit v1.2.3