diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | ExampleAssessments/CMP-4009B.yml | 6 | ||||
| -rw-r--r-- | ExampleAssessments/smol.yml | 21 | ||||
| -rw-r--r-- | mark.py | 46 | ||||
| -rw-r--r-- | reflect.py | 60 | ||||
| -rw-r--r-- | reportWriter.py | 36 | 
6 files changed, 165 insertions, 6 deletions
| @@ -1,3 +1,5 @@ +*_report.md +  # Byte-compiled / optimized / DLL files  __pycache__/  *.py[cod] diff --git a/ExampleAssessments/CMP-4009B.yml b/ExampleAssessments/CMP-4009B.yml index 373f7f4..571c97f 100644 --- a/ExampleAssessments/CMP-4009B.yml +++ b/ExampleAssessments/CMP-4009B.yml @@ -17,8 +17,12 @@ files:      - tester.py:          functions:              - dateTester(2) +        printIfExecuted: False      - turbine.py:          classes:              - Turbine:                  attributes: -                    - rating:int
\ No newline at end of file +                    - rating:int +    - aNonExistantModule.py: +        functions: +            - aNonExistantFunction(1)
\ No newline at end of file diff --git a/ExampleAssessments/smol.yml b/ExampleAssessments/smol.yml new file mode 100644 index 0000000..d53e721 --- /dev/null +++ b/ExampleAssessments/smol.yml @@ -0,0 +1,21 @@ +files: +    - pjtool.py: +        classes: +            - Date: +                methods: +                    - __init__(4) +                    - __str__(1) +                    - __eq__(2) +                    - __lt__(2) +                    - numYears(2) +                    - numMonths(2) +            - AnotherClass: +                    - floofleBerries(2) +        tests: +            - | +              d1 = Date(2001, 8, 12) +              d2 = Date(2001, 8, 12) +              return d1 == d2 +    - aNonExistantModule.py: +        functions: +            - aNonExistantFunction(1)
\ No newline at end of file @@ -1,6 +1,8 @@ +import reportWriter  import argparse  import tempfile  import zipfile +import reflect  import yaml  import os @@ -10,11 +12,47 @@ def main(assessment_path, submission_path, student_no):      with open(assessment_path, "r") as f:          assessment_struct = yaml.safe_load(f) -    print(assessment_struct) - -    for required_file in assessment_struct["files"]: +    reflection = reflect.Reflect(submission_path) +    present_module_names = [i.name for i in reflection.client_modules] +    writer = reportWriter.MarkDownReportWriter(student_no) +     +    for i, required_file in enumerate(assessment_struct["files"], 0):          required_file = list(required_file.keys())[0] -        print(required_file, required_file in os.listdir(submission_path)) +        module_name = os.path.splitext(required_file)[0] + +        if module_name not in present_module_names: +            writer.append_module(module_name, False) +            continue +         +        reflection.import_module(module_name) +        writer.append_module(module_name, True, reflection.get_module_doc(module_name)) + +        this_files_features = assessment_struct["files"][i][required_file] +        if "classes" in this_files_features.keys(): + +            present_classes = reflection.get_classes(module_name) +            for j, class_name in enumerate(this_files_features["classes"], 0): +                class_name = list(class_name.keys())[0] +                 +                if class_name not in present_classes.keys(): +                    writer.append_class(class_name, False) +                    continue + +                writer.append_class(class_name, True, present_classes[class_name][1]) + +                present_methods = reflection.get_class_methods(module_name, class_name) +                print(present_methods) +                for required_method in this_files_features["classes"][j][class_name]["methods"]: +                    print(required_method) + +    # print(submission_path) +    # reflection = reflect.Reflect(submission_path) +    # # reflection.import_module("pjtool") +    # # print(reflection.get_classes("pjtool")) +    # # print(reflection.get_class_methods("pjtool", "Date")["__eq__"]) +    # reflection.import_module("tester") +    # print(reflection.get_functions("tester")) +  if __name__ == "__main__":      parser = argparse.ArgumentParser() @@ -15,17 +15,75 @@ class Reflect:          self.client_modules = [p for p in pkgutil.iter_modules() if str(p[0])[12:-2] == self.client_code_path]      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:                  self.imported_modules[module_name] = importlib.import_module(module.name)      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 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], inspect.getdoc(i[1]))  +            for i in inspect.getmembers(self.imported_modules[module_name])  +            if inspect.isclass(i[1]) +        } + +    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 a list of the named arguments.  +            WARNING: Does not deal with *args and **kwargs and stuff. +        """ +        return { +            i[0]: (i[1], inspect.getdoc(i[1]), inspect.getfullargspec(i[1])[0])  +            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], inspect.getdoc(i[1]), inspect.getfullargspec(i[1])[0])  +            for i in inspect.getmembers(self.imported_modules[module_name])  +            if inspect.isfunction(i[1]) +        }  if __name__ == "__main__":      user_code_path = "/media/veracrypt1/Nextcloud/UniStuff/3.0 - CMP 3rd Year Project/ExampleSubmissions/Submission_A"      reflect = Reflect(user_code_path)      reflect.import_module("pjtool") -    print(reflect.get_module_doc("pjtool")) +    print(reflect.get_class_methods("pjtool", "Date")) diff --git a/reportWriter.py b/reportWriter.py index e69de29..ebbfecf 100644 --- a/reportWriter.py +++ b/reportWriter.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +import datetime + +@dataclass +class MarkDownReportWriter: +    student_no:str + +    def __post_init__(self): +        self.__push_line(""" +# %s Submission Report + +Report automatically generated at %s + +## Files\n\n""" % (self.student_no, datetime.datetime.now())) + +    def __push_line(self, line): +        with open("%s_report.md" % self.student_no, "a") as f: +            f.write(line) + +    def append_module(self, module_name, found = True, docs = None): +        self.__push_line("### File: `%s.py`\n\n" % module_name) +        if found: +            self.__push_line(" - [x] Present\n") +            if len(docs) > 2: +                self.__push_line(" - [x] Documented (%d characters)\n\n" % (len(docs))) +        else: +            self.__push_line(" - [ ] Present\n\n") + +    def append_class(self, class_name, found = True, docs = None): +        self.__push_line("#### Class: `%s`\n\n" % class_name) +        if found: +            self.__push_line(" - [x] Present\n") +            if len(docs) > 2: +                self.__push_line(" - [x] Documented (%d characters)\n\n" % (len(docs))) +        else: +            self.__push_line(" - [ ] Present\n\n")
\ No newline at end of file | 
