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 --- .gitmodules | 4 +- 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 ++++++++++++++++ database.py | 97 ---------- examplerun.bat | 3 - examplerun.sh | 12 +- jinja_helpers.py | 132 ------------- mark.py | 93 --------- misc_classes.py | 99 ---------- pytest_template.jinja2 | 8 - python-latex-highlighting | 1 - reflect.py | 395 -------------------------------------- requirements.txt | 10 - smarker.conf | 24 --- templates/markdown.jinja2 | Bin 26 -> 0 bytes templates/md.jinja2 | 166 ---------------- templates/tex.jinja2 | 249 ------------------------ templates/text.jinja2 | Bin 28 -> 0 bytes templates/txt.jinja2 | 168 ---------------- 31 files changed, 1449 insertions(+), 1453 deletions(-) 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 delete mode 100644 database.py delete mode 100644 examplerun.bat delete mode 100644 jinja_helpers.py delete mode 100644 mark.py delete mode 100644 misc_classes.py delete mode 100644 pytest_template.jinja2 delete mode 160000 python-latex-highlighting delete mode 100644 reflect.py delete mode 100644 requirements.txt delete mode 100644 smarker.conf delete mode 120000 templates/markdown.jinja2 delete mode 100644 templates/md.jinja2 delete mode 100644 templates/tex.jinja2 delete mode 120000 templates/text.jinja2 delete mode 100644 templates/txt.jinja2 diff --git a/.gitmodules b/.gitmodules index 3183235..63fc1f6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "python-latex-highlighting"] - path = python-latex-highlighting +[submodule "Smarker/python-latex-highlighting"] + path = Smarker/python-latex-highlighting url = git@github.com:olivierverdier/python-latex-highlighting.git 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 diff --git a/database.py b/database.py deleted file mode 100644 index a0e3640..0000000 --- a/database.py +++ /dev/null @@ -1,97 +0,0 @@ -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/examplerun.bat b/examplerun.bat deleted file mode 100644 index 00b0a80..0000000 --- a/examplerun.bat +++ /dev/null @@ -1,3 +0,0 @@ -zip -r 100301654.zip .\ExampleSubmission\ -python .\mark.py -s 100301654.zip -a .\ExampleAssessments\example.yml -f yaml -o auto -rm 100301654.zip \ No newline at end of file diff --git a/examplerun.sh b/examplerun.sh index b78046f..1f3bb23 100644 --- a/examplerun.sh +++ b/examplerun.sh @@ -1,8 +1,8 @@ zip -r 100301654.zip ./ExampleSubmission/ -python ./mark.py -s 100301654.zip -a ./ExampleAssessments/example.yml -f tex -o auto +python ./Smarker/mark.py -s 100301654.zip -a ./ExampleAssessments/example.yml -f pdf -o auto rm 100301654.zip -pdflatex 100301654_report.tex -rm -v *.log -rm -v *.aux -# rm -v *.tex -rm -v *_test_report.pdf \ No newline at end of file +# pdflatex 100301654_report.tex +# rm -v *.log +# rm -v *.aux +# # rm -v *.tex +# rm -v *_test_report.pdf \ No newline at end of file diff --git a/jinja_helpers.py b/jinja_helpers.py deleted file mode 100644 index a9e0e2e..0000000 --- a/jinja_helpers.py +++ /dev/null @@ -1,132 +0,0 @@ -"""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_helpers(): - import reflect - import os - - r = reflect.Reflect(os.getcwd()) - 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_required_num_args("aFunctionThatIsntThere(2)")) - - - \ No newline at end of file diff --git a/mark.py b/mark.py deleted file mode 100644 index 720553b..0000000 --- a/mark.py +++ /dev/null @@ -1,93 +0,0 @@ -from dataclasses import dataclass -import jinja_helpers -import configparser -import misc_classes -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("templates", "%s.jinja2" % kwargs["format"]) - if kwargs["format"] == "tex": - 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) - - # input("\n\n[tempdir: %s] Press any key to close..." % tempdir) - -if __name__ == "__main__": - config = configparser.ConfigParser() - config.read("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"] + [os.path.splitext(f)[0] for f in os.listdir("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/misc_classes.py b/misc_classes.py deleted file mode 100644 index 5bf16c1..0000000 --- a/misc_classes.py +++ /dev/null @@ -1,99 +0,0 @@ -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('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/pytest_template.jinja2 b/pytest_template.jinja2 deleted file mode 100644 index 73c9a40..0000000 --- a/pytest_template.jinja2 +++ /dev/null @@ -1,8 +0,0 @@ -{# 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/python-latex-highlighting b/python-latex-highlighting deleted file mode 160000 index a5b8353..0000000 --- a/python-latex-highlighting +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a5b8353876512d8d571a3c3be59452995318a177 diff --git a/reflect.py b/reflect.py deleted file mode 100644 index dbe551f..0000000 --- a/reflect.py +++ /dev/null @@ -1,395 +0,0 @@ -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("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/requirements.txt b/requirements.txt deleted file mode 100644 index 3831b5e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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.conf b/smarker.conf deleted file mode 100644 index a31e5d7..0000000 --- a/smarker.conf +++ /dev/null @@ -1,24 +0,0 @@ -[mysql] -host = 192.168.1.92 -port = 3306 -user = root -passwd = ************* - -[tex] -columns = 1 -show_full_docs = True -show_source = True -show_all_regex_occurrences = True -show_all_run_output = True - -[md] -show_full_docs = False -show_source = False -show_all_regex_occurrences = True -show_all_run_output = False - -[txt] -show_full_docs = False -show_source = False -show_all_regex_occurrences = True -show_all_run_output = False \ No newline at end of file diff --git a/templates/markdown.jinja2 b/templates/markdown.jinja2 deleted file mode 120000 index 99c26ce..0000000 Binary files a/templates/markdown.jinja2 and /dev/null differ diff --git a/templates/md.jinja2 b/templates/md.jinja2 deleted file mode 100644 index e764a49..0000000 --- a/templates/md.jinja2 +++ /dev/null @@ -1,166 +0,0 @@ -{%- 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/templates/tex.jinja2 b/templates/tex.jinja2 deleted file mode 100644 index 94deb20..0000000 --- a/templates/tex.jinja2 +++ /dev/null @@ -1,249 +0,0 @@ -((* 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{python-latex-highlighting/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/templates/text.jinja2 b/templates/text.jinja2 deleted file mode 120000 index eca6ebd..0000000 Binary files a/templates/text.jinja2 and /dev/null differ diff --git a/templates/txt.jinja2 b/templates/txt.jinja2 deleted file mode 100644 index 9eb4beb..0000000 --- a/templates/txt.jinja2 +++ /dev/null @@ -1,168 +0,0 @@ -{%- 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