|
| 1 | +"""This module provides a utility function for collecting the core information of all Grader classes.""" |
| 2 | + |
| 3 | +import ast |
| 4 | +import json |
| 5 | +import re |
| 6 | +import time |
| 7 | +from copy import deepcopy |
| 8 | +from pathlib import Path |
| 9 | +from queue import Queue |
| 10 | +from typing import Any, Dict, List |
| 11 | + |
| 12 | +from loguru import logger |
| 13 | + |
| 14 | + |
| 15 | +class _MethodInfo: |
| 16 | + def __init__(self, signature: str = "", docstring: str = ""): |
| 17 | + # covers name, arguments, and maybe return type |
| 18 | + self.signature = signature or "" |
| 19 | + self.docstring = docstring or "" |
| 20 | + |
| 21 | + def __str__(self): |
| 22 | + return json.dumps(self.__dict__, ensure_ascii=False) |
| 23 | + |
| 24 | + |
| 25 | +class _GraderInfo: |
| 26 | + """A class that stores core information of a Grader class""" |
| 27 | + |
| 28 | + def __init__( |
| 29 | + self, |
| 30 | + file_path: str = "", |
| 31 | + class_name: str = "", |
| 32 | + parent_class_names: list = None, |
| 33 | + init_method: _MethodInfo = None, |
| 34 | + aevaluate_method: _MethodInfo = None, |
| 35 | + ): |
| 36 | + self.file_path = file_path or "" |
| 37 | + self.class_name = class_name or "" |
| 38 | + self.parent_class_names = parent_class_names or [] |
| 39 | + self.init_method = init_method or _MethodInfo() |
| 40 | + self.aevaluate_method = aevaluate_method or _MethodInfo() |
| 41 | + |
| 42 | + def __str__(self): |
| 43 | + d = deepcopy(self.__dict__) |
| 44 | + d["init_method"] = self.init_method.__dict__ |
| 45 | + d["aevaluate_method"] = self.aevaluate_method.__dict__ |
| 46 | + return json.dumps(d, ensure_ascii=False) |
| 47 | + |
| 48 | + def __iter__(self): |
| 49 | + """Customize dict(obj) result""" |
| 50 | + yield "file_path", self.file_path |
| 51 | + yield "class_name", self.class_name |
| 52 | + yield "parent_class_names", self.parent_class_names |
| 53 | + yield "init_method", self.init_method.__dict__ |
| 54 | + yield "aevaluate_method", self.aevaluate_method.__dict__ |
| 55 | + |
| 56 | + |
| 57 | +def get_all_grader_info() -> List[Dict[str, Any]]: |
| 58 | + """Collect the information of all graders defined under the openjudge/graders folder.""" |
| 59 | + t0 = time.time_ns() |
| 60 | + current_file_abs_path = Path(__file__).resolve() |
| 61 | + logger.info(f"grader_info.py path:{current_file_abs_path}") |
| 62 | + |
| 63 | + defs_of_classes_having_parent = {} |
| 64 | + grader_root_folder = Path(current_file_abs_path.parent.parent, "graders") |
| 65 | + logger.info(f"grader root folder:{grader_root_folder}") |
| 66 | + unprocessed_folders = Queue() |
| 67 | + unprocessed_folders.put(grader_root_folder) |
| 68 | + while not unprocessed_folders.empty(): |
| 69 | + folder = unprocessed_folders.get() |
| 70 | + for item in folder.iterdir(): |
| 71 | + if item.is_dir(): |
| 72 | + unprocessed_folders.put(item) |
| 73 | + elif item.stem == "__init__" or item.suffix != ".py": |
| 74 | + # use safe heuristics to reduce candidate count |
| 75 | + continue |
| 76 | + else: |
| 77 | + _get_defs_of_classes_having_parent(item, defs_of_classes_having_parent) |
| 78 | + |
| 79 | + logger.info(f"classes having parent:{len(defs_of_classes_having_parent)}, {(time.time_ns() - t0)/1000000}ms") |
| 80 | + |
| 81 | + all_grader_class_defs = _get_grader_class_def(defs_of_classes_having_parent) |
| 82 | + |
| 83 | + t0 = time.time_ns() |
| 84 | + all_grader_info = [] |
| 85 | + for _, (class_def, source_code) in all_grader_class_defs.items(): |
| 86 | + grader_info = _parse_grader_class_def(class_def, source_code) |
| 87 | + all_grader_info.append(dict(grader_info)) |
| 88 | + |
| 89 | + logger.info(f"all grader info:{len(all_grader_info)}, {(time.time_ns() - t0)/1000000}ms") |
| 90 | + |
| 91 | + return all_grader_info |
| 92 | + |
| 93 | + |
| 94 | +def _get_defs_of_classes_having_parent(py_file: Path, defs_of_classes_having_parent: Dict[ast.ClassDef, str]): |
| 95 | + """Get the definitions and source codes of all classe that have parent class.""" |
| 96 | + with open(py_file, "r", encoding="utf-8") as file: |
| 97 | + source_code = file.read() |
| 98 | + |
| 99 | + # Parse the source code into an AST node |
| 100 | + tree = ast.parse(source_code) |
| 101 | + for node in ast.walk(tree): |
| 102 | + if isinstance(node, ast.ClassDef) and node.bases: |
| 103 | + parent_count = len(node.bases) |
| 104 | + if parent_count > 1 or node.bases[0].id != "ABC": |
| 105 | + defs_of_classes_having_parent[node] = source_code |
| 106 | + |
| 107 | + |
| 108 | +def _get_grader_class_def(defs_of_classes_having_parent: Dict[ast.ClassDef, str]): |
| 109 | + """Get the definitions of Grader classes""" |
| 110 | + t0 = time.time_ns() |
| 111 | + # the base grader class as seed |
| 112 | + all_grader_class_defs = {"BaseGrader": None} |
| 113 | + found_grader = True |
| 114 | + while found_grader: |
| 115 | + found_grader = False |
| 116 | + known_defs = [] |
| 117 | + new_defs = [] |
| 118 | + |
| 119 | + for class_def, source_code in defs_of_classes_having_parent.items(): |
| 120 | + # Skip whose already processed |
| 121 | + if class_def.name in all_grader_class_defs: |
| 122 | + known_defs.append(class_def) |
| 123 | + continue |
| 124 | + |
| 125 | + for parent in class_def.bases: |
| 126 | + if parent.id in all_grader_class_defs: |
| 127 | + all_grader_class_defs[class_def.name] = (class_def, source_code) |
| 128 | + new_defs.append(class_def) |
| 129 | + found_grader = True |
| 130 | + break |
| 131 | + |
| 132 | + # reduce inputs for the next round, by removing those processed in this round |
| 133 | + for n in known_defs: |
| 134 | + defs_of_classes_having_parent.pop(n) |
| 135 | + for n in new_defs: |
| 136 | + defs_of_classes_having_parent.pop(n) |
| 137 | + |
| 138 | + # remove the seed |
| 139 | + all_grader_class_defs.pop("BaseGrader") |
| 140 | + |
| 141 | + logger.info(f"all grader class defs:{len(all_grader_class_defs)}, {(time.time_ns() - t0)/1000000}ms") |
| 142 | + return all_grader_class_defs |
| 143 | + |
| 144 | + |
| 145 | +_INIT_METHOD = "__init__" |
| 146 | +_AEVALUATE_METHOD = "aevaluate" |
| 147 | +_TARGET_METHODS = set() |
| 148 | +_TARGET_METHODS.add(_INIT_METHOD) |
| 149 | +_TARGET_METHODS.add(_AEVALUATE_METHOD) |
| 150 | + |
| 151 | +_NEWLINE_OR_MULTI_SPACE_PATTERN = re.compile(r"(\n|\s\s+)") |
| 152 | + |
| 153 | + |
| 154 | +def _parse_grader_class_def(class_def: ast.ClassDef, source_code: str) -> _GraderInfo: |
| 155 | + """Use ast util to extract core information from a Grader class, |
| 156 | + and put the information into a _GraderInfo object.""" |
| 157 | + |
| 158 | + init_method = _MethodInfo() |
| 159 | + aeval_method = _MethodInfo() |
| 160 | + # Find target methods within the class body |
| 161 | + for sub_node in class_def.body: |
| 162 | + if isinstance(sub_node, (ast.FunctionDef, ast.AsyncFunctionDef)): |
| 163 | + method_name = sub_node.name |
| 164 | + if method_name not in _TARGET_METHODS: |
| 165 | + continue |
| 166 | + |
| 167 | + segment = ast.get_source_segment(source_code, sub_node) |
| 168 | + segment = _NEWLINE_OR_MULTI_SPACE_PATTERN.sub(" ", segment.strip()) |
| 169 | + segment = segment.replace(") :", "):").replace(") ->", ")->").replace(" :", ":") |
| 170 | + |
| 171 | + # Method head ends in two ways: w/ or w/o return type annocation. |
| 172 | + # Figure it out. |
| 173 | + idx0 = segment.find("):") |
| 174 | + idx1 = segment.find(")->") |
| 175 | + if idx1 > 0: |
| 176 | + idx1 = segment.find(":", idx1) |
| 177 | + |
| 178 | + if idx0 > 0 and idx1 > 0: |
| 179 | + if idx0 < idx1: |
| 180 | + idx = idx0 + 2 |
| 181 | + else: |
| 182 | + idx = idx1 + 1 |
| 183 | + elif idx0 > 0: |
| 184 | + idx = idx0 + 2 |
| 185 | + elif idx1 > 0: |
| 186 | + idx = idx1 + 1 |
| 187 | + else: |
| 188 | + idx = -1 |
| 189 | + |
| 190 | + # def foo(...) -> type: |
| 191 | + # def foo(...): |
| 192 | + if idx > 0: |
| 193 | + signature = segment[0:idx] |
| 194 | + else: |
| 195 | + signature = "SIGNATURE_NOT_FOUND" |
| 196 | + |
| 197 | + docstring = ast.get_docstring(sub_node) |
| 198 | + if method_name == _INIT_METHOD: |
| 199 | + init_method.signature = signature |
| 200 | + init_method.docstring = docstring |
| 201 | + elif method_name == _AEVALUATE_METHOD: |
| 202 | + aeval_method.signature = signature |
| 203 | + aeval_method.docstring = docstring |
| 204 | + |
| 205 | + grader_info_obj = _GraderInfo( |
| 206 | + class_name=class_def.name, |
| 207 | + parent_class_names=[p.id for p in class_def.bases], |
| 208 | + init_method=init_method, |
| 209 | + aevaluate_method=aeval_method, |
| 210 | + ) |
| 211 | + |
| 212 | + return grader_info_obj |
| 213 | + |
| 214 | + |
| 215 | +if __name__ == "__main__": |
| 216 | + graders = get_all_grader_info() |
| 217 | + print("-------------") |
| 218 | + print(f"{len(graders)} graders") |
| 219 | + for g_i in graders: |
| 220 | + print("-------------") |
| 221 | + print(type(g_i)) |
| 222 | + print(g_i) |
0 commit comments