autograder.util.prepare_submission
1import os 2import types 3import typing 4 5import edq.util.code 6 7ALL_SUBMISSION_KEY: str = '__all__' 8ALLOWED_EXTENSIONS: typing.List[str] = ['.py', '.ipynb'] 9 10def prepare(path: str, raise_on_collision: bool = False) -> object: 11 """ 12 Get a submission from a path, prepare it for grading, 13 and return a submission namespace that contains all parsed entities. 14 15 Directories will be fully recursively descended and each python or notebook will be prepared. 16 17 Entries that begin with two underscores will not be included. 18 19 The following things will be done to each file: 20 1) The code will be parsed and sanitized. 21 2) The sanitized code will be imported as a module into a namespace. 22 3) The namespace will be made available in the returned namespace 23 in a structure that matches its package structure. 24 E.g. "./a/b/c.py" will be available from `prepare('.').a.b.c`, 25 and "foo.py" will be available from `prepare('foo.py').foo`. 26 4) All entries in the module will be put in the ALL_SUBMISSION_KEY attribute of the 27 returned namespace. 28 If raise_on_collision is True, an error will be raised if a key already exists. 29 """ 30 31 submission: typing.Dict[str, typing.Any] = {} 32 33 if (os.path.isfile(path)): 34 _prepare_submission_file(submission, path, [], raise_on_collision) 35 else: 36 _prepare_submission_dir(submission, path, [], raise_on_collision) 37 38 return _dict_to_namespace(submission) 39 40def _prepare_submission_dir( 41 submission: typing.Dict[str, typing.Any], 42 path: str, 43 prefix: typing.List[str], 44 raise_on_collision: bool, 45 ) -> None: 46 """ Prepare a submission directory. """ 47 48 if (not os.path.isdir(path)): 49 raise ValueError(f"Preparation target must be a dir: '{path}'.") 50 51 for dirent in os.listdir(path): 52 dirent_path = os.path.join(path, dirent) 53 54 if (os.path.isfile(dirent_path)): 55 if (os.path.splitext(dirent)[1] not in ALLOWED_EXTENSIONS): 56 continue 57 58 _prepare_submission_file(submission, dirent_path, prefix, raise_on_collision) 59 else: 60 _prepare_submission_dir(submission, dirent_path, prefix + [dirent], raise_on_collision) 61 62def _prepare_submission_file( 63 submission: typing.Dict[str, typing.Any], 64 path: str, 65 prefix: typing.List[str], 66 raise_on_collision: bool, 67 ) -> None: 68 """ Prepare a submission file. """ 69 70 if (not os.path.isfile(path)): 71 raise ValueError(f"Preparation target must be a file: '{path}'.") 72 73 basename = os.path.splitext(os.path.basename(path))[0] 74 75 defs = vars(edq.util.code.sanitize_and_import_path(path)) 76 77 for (name, value) in defs.items(): 78 if (name.startswith('__')): 79 continue 80 81 if ((name in submission) and (raise_on_collision)): 82 raise ValueError(f"Name collision ('{name}') when importing all keys for '{path}'.") 83 84 if (ALL_SUBMISSION_KEY not in submission): 85 submission[ALL_SUBMISSION_KEY] = {} 86 87 submission[ALL_SUBMISSION_KEY][name] = value 88 89 # Place the defs into a structure that matches the path. 90 context = submission 91 for part in prefix + [basename]: 92 if ((part in context) and (not isinstance(context[part], dict))): 93 raise ValueError(f"Name collision ('{part}') when importing all keys for '{path}'.") 94 95 if (part not in context): 96 context[part] = {} 97 98 context = context[part] 99 100 context.update(defs) 101 102def _dict_to_namespace(root: typing.Union[typing.Dict, object]) -> object: 103 """ Recursively convert a dict to a namespace. """ 104 105 if (not isinstance(root, dict)): 106 return root 107 108 for (key, value) in root.items(): 109 root[key] = _dict_to_namespace(value) 110 111 return types.SimpleNamespace(**root)
11def prepare(path: str, raise_on_collision: bool = False) -> object: 12 """ 13 Get a submission from a path, prepare it for grading, 14 and return a submission namespace that contains all parsed entities. 15 16 Directories will be fully recursively descended and each python or notebook will be prepared. 17 18 Entries that begin with two underscores will not be included. 19 20 The following things will be done to each file: 21 1) The code will be parsed and sanitized. 22 2) The sanitized code will be imported as a module into a namespace. 23 3) The namespace will be made available in the returned namespace 24 in a structure that matches its package structure. 25 E.g. "./a/b/c.py" will be available from `prepare('.').a.b.c`, 26 and "foo.py" will be available from `prepare('foo.py').foo`. 27 4) All entries in the module will be put in the ALL_SUBMISSION_KEY attribute of the 28 returned namespace. 29 If raise_on_collision is True, an error will be raised if a key already exists. 30 """ 31 32 submission: typing.Dict[str, typing.Any] = {} 33 34 if (os.path.isfile(path)): 35 _prepare_submission_file(submission, path, [], raise_on_collision) 36 else: 37 _prepare_submission_dir(submission, path, [], raise_on_collision) 38 39 return _dict_to_namespace(submission)
Get a submission from a path, prepare it for grading, and return a submission namespace that contains all parsed entities.
Directories will be fully recursively descended and each python or notebook will be prepared.
Entries that begin with two underscores will not be included.
The following things will be done to each file:
1) The code will be parsed and sanitized.
2) The sanitized code will be imported as a module into a namespace.
3) The namespace will be made available in the returned namespace
in a structure that matches its package structure.
E.g. "./a/b/c.py" will be available from prepare('.').a.b.c,
and "foo.py" will be available from prepare('foo.py').foo.
4) All entries in the module will be put in the ALL_SUBMISSION_KEY attribute of the
returned namespace.
If raise_on_collision is True, an error will be raised if a key already exists.