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)
ALL_SUBMISSION_KEY: str = '__all__'
ALLOWED_EXTENSIONS: List[str] = ['.py', '.ipynb']
def prepare(path: str, raise_on_collision: bool = False) -> object:
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.