From d262c125550dfb952abeb1c953731f470c52decd Mon Sep 17 00:00:00 2001
From: jwansek <eddie.atten.ea29@gmail.com>
Date: Mon, 15 Nov 2021 19:46:31 +0000
Subject: made a start on analysis

---
 .gitignore                       |  2 ++
 ExampleAssessments/CMP-4009B.yml |  6 +++-
 ExampleAssessments/smol.yml      | 21 ++++++++++++++
 mark.py                          | 46 +++++++++++++++++++++++++++---
 reflect.py                       | 60 +++++++++++++++++++++++++++++++++++++++-
 reportWriter.py                  | 36 ++++++++++++++++++++++++
 6 files changed, 165 insertions(+), 6 deletions(-)
 create mode 100644 ExampleAssessments/smol.yml

diff --git a/.gitignore b/.gitignore
index b6e4761..70f2228 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/mark.py b/mark.py
index f1ffa59..8078b62 100644
--- a/mark.py
+++ b/mark.py
@@ -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()
diff --git a/reflect.py b/reflect.py
index 2cfb2f3..1cb6241 100644
--- a/reflect.py
+++ b/reflect.py
@@ -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
-- 
cgit v1.2.3