128Autograder 5.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. 128autograder-5.0.1/PKG-INFO +15 -0
  2. 128autograder-5.0.1/pyproject.toml +97 -0
  3. 128autograder-5.0.1/setup.cfg +4 -0
  4. 128autograder-5.0.1/source/128Autograder.egg-info/PKG-INFO +15 -0
  5. 128autograder-5.0.1/source/128Autograder.egg-info/SOURCES.txt +47 -0
  6. 128autograder-5.0.1/source/128Autograder.egg-info/dependency_links.txt +1 -0
  7. 128autograder-5.0.1/source/128Autograder.egg-info/entry_points.txt +7 -0
  8. 128autograder-5.0.1/source/128Autograder.egg-info/requires.txt +10 -0
  9. 128autograder-5.0.1/source/128Autograder.egg-info/top_level.txt +2 -0
  10. 128autograder-5.0.1/source/autograder_cli/__init__.py +0 -0
  11. 128autograder-5.0.1/source/autograder_cli/build_autograder.py +314 -0
  12. 128autograder-5.0.1/source/autograder_cli/create_upload.py +103 -0
  13. 128autograder-5.0.1/source/autograder_cli/run_gradescope.py +108 -0
  14. 128autograder-5.0.1/source/autograder_cli/run_local.py +299 -0
  15. 128autograder-5.0.1/source/autograder_cli/run_prairielearn.py +59 -0
  16. 128autograder-5.0.1/source/autograder_platform/Executors/Environment.py +273 -0
  17. 128autograder-5.0.1/source/autograder_platform/Executors/Executor.py +64 -0
  18. 128autograder-5.0.1/source/autograder_platform/Executors/__init__.py +0 -0
  19. 128autograder-5.0.1/source/autograder_platform/Executors/common.py +55 -0
  20. 128autograder-5.0.1/source/autograder_platform/StudentSubmission/AbstractStudentSubmission.py +178 -0
  21. 128autograder-5.0.1/source/autograder_platform/StudentSubmission/AbstractValidator.py +28 -0
  22. 128autograder-5.0.1/source/autograder_platform/StudentSubmission/GenericValidators.py +43 -0
  23. 128autograder-5.0.1/source/autograder_platform/StudentSubmission/ISubmissionProcess.py +27 -0
  24. 128autograder-5.0.1/source/autograder_platform/StudentSubmission/SubmissionProcessFactory.py +66 -0
  25. 128autograder-5.0.1/source/autograder_platform/StudentSubmission/__init__.py +0 -0
  26. 128autograder-5.0.1/source/autograder_platform/StudentSubmission/common.py +60 -0
  27. 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/AbstractPythonImportFactory.py +11 -0
  28. 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonEnvironment.py +122 -0
  29. 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonFileImportFactory.py +62 -0
  30. 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonModuleMockImportFactory.py +37 -0
  31. 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonSubmission.py +204 -0
  32. 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonSubmissionProcess.py +358 -0
  33. 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonValidators.py +130 -0
  34. 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/Runners.py +278 -0
  35. 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/__init__.py +7 -0
  36. 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/common.py +46 -0
  37. 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/__init__.py +0 -0
  38. 128autograder-5.0.1/source/autograder_platform/Tasks/Task.py +51 -0
  39. 128autograder-5.0.1/source/autograder_platform/Tasks/TaskRunner.py +75 -0
  40. 128autograder-5.0.1/source/autograder_platform/Tasks/__init__.py +0 -0
  41. 128autograder-5.0.1/source/autograder_platform/Tasks/common.py +26 -0
  42. 128autograder-5.0.1/source/autograder_platform/TestingFramework/Assertions.py +177 -0
  43. 128autograder-5.0.1/source/autograder_platform/TestingFramework/SingleFunctionMock.py +69 -0
  44. 128autograder-5.0.1/source/autograder_platform/TestingFramework/__init__.py +0 -0
  45. 128autograder-5.0.1/source/autograder_platform/__init__.py +1 -0
  46. 128autograder-5.0.1/source/autograder_platform/cli.py +85 -0
  47. 128autograder-5.0.1/source/autograder_platform/config/Config.py +365 -0
  48. 128autograder-5.0.1/source/autograder_platform/config/__init__.py +0 -0
  49. 128autograder-5.0.1/source/autograder_platform/config/common.py +22 -0
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.1
2
+ Name: 128Autograder
3
+ Version: 5.0.1
4
+ Author: Gregory Bell
5
+ Maintainer: Gregory Bell
6
+ Requires-Python: >=3.11.0
7
+ Requires-Dist: HybridJSONTestRunner==0.8.1
8
+ Requires-Dist: dill==0.3.6
9
+ Requires-Dist: Better-PyUnit-Format==0.2.3
10
+ Requires-Dist: schema==0.7.5
11
+ Requires-Dist: requests==2.31.0
12
+ Requires-Dist: tomli==2.0.1
13
+ Provides-Extra: dev
14
+ Requires-Dist: coverage[toml]; extra == "dev"
15
+ Requires-Dist: build; extra == "dev"
@@ -0,0 +1,97 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools.packages.find]
6
+ where=["source"]
7
+ exclude =["tests"]
8
+
9
+ [tool.setuptools.dynamic]
10
+ version = {attr = "autograder_platform.__version__"}
11
+
12
+ [project]
13
+ name = "128Autograder"
14
+ authors = [
15
+ { name = "Gregory Bell" }
16
+ ]
17
+ maintainers = [
18
+ { name = "Gregory Bell" }
19
+ ]
20
+
21
+ dynamic = ["version"]
22
+
23
+ requires-python = ">=3.11.0"
24
+
25
+ dependencies = [
26
+ "HybridJSONTestRunner==0.8.1",
27
+ "dill==0.3.6",
28
+ "Better-PyUnit-Format==0.2.3",
29
+ "schema==0.7.5",
30
+ "requests==2.31.0",
31
+ "tomli==2.0.1",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "coverage[toml]",
37
+ "build",
38
+ ]
39
+
40
+ [project.scripts]
41
+ run_gradescope = "autograder_cli.run_gradescope:tool"
42
+ run_prairielearn = "autograder_cli.run_prairielearn:tool"
43
+ test_my_work = "autograder_cli.run_local:tool"
44
+ run_autograder = "autograder_cli.run_local:tool"
45
+ create_gradescope_upload = "autograder_cli.create_upload:tool"
46
+ build_autograder = "autograder_cli.build_autograder:tool"
47
+
48
+ [tool.pyright]
49
+ include = ["source"]
50
+
51
+ exclude = ["**__pycache__**"]
52
+ scrict = ["."]
53
+
54
+ executionEnvironments = [
55
+ { root = "tests", extraPaths = ["source", "source/utils/student", "source/utils"] }
56
+ ]
57
+
58
+ [tool.coverage.run]
59
+ command_line = "-m unittest"
60
+ concurrency = ["multiprocessing"]
61
+ # Omit testing files, omit dynamic files, omit __init__.py
62
+ # Doing it at the run level as it will that means they wont be run in the first place
63
+ # Ignore build.py as its it is primarly covered by integration tests
64
+ omit = [
65
+ # TODO write unit tests for the CLI
66
+ # shouldnt be ignoring them, but also this needs to get out the door
67
+ # Technically the CLIs are covered via the e2e tests, and we dont seem to have a hole there anymore
68
+ "source/autograder_cli/*",
69
+ "sandbox/*",
70
+ "testPrograms/*",
71
+ "test_code",
72
+ "test_code.py",
73
+ "setup_code",
74
+ "tests/*",
75
+ "*/student_submission",
76
+ "__init__.py",
77
+ "Build.py",
78
+ "INJECTED_*",
79
+ "__getstate__*",
80
+ "__setstate__*",
81
+ ]
82
+
83
+ [tool.coverage.report]
84
+ fail_under = 90
85
+ skip_empty = true
86
+ exclude_also = [
87
+ # Dont flag error conditions that cant be reached or that are just defensive
88
+ "raise AssertionError",
89
+ "raise NotImplementedError",
90
+ "raise EnvironmentError",
91
+ "raise AttributeError",
92
+ "raise InvalidRunner",
93
+ "if __name__ == .__main__.:",
94
+ # Don't complain about abstract methods, they aren't run:
95
+ "@(abc\\.)?abstractmethod",
96
+ ]
97
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.1
2
+ Name: 128Autograder
3
+ Version: 5.0.1
4
+ Author: Gregory Bell
5
+ Maintainer: Gregory Bell
6
+ Requires-Python: >=3.11.0
7
+ Requires-Dist: HybridJSONTestRunner==0.8.1
8
+ Requires-Dist: dill==0.3.6
9
+ Requires-Dist: Better-PyUnit-Format==0.2.3
10
+ Requires-Dist: schema==0.7.5
11
+ Requires-Dist: requests==2.31.0
12
+ Requires-Dist: tomli==2.0.1
13
+ Provides-Extra: dev
14
+ Requires-Dist: coverage[toml]; extra == "dev"
15
+ Requires-Dist: build; extra == "dev"
@@ -0,0 +1,47 @@
1
+ pyproject.toml
2
+ source/128Autograder.egg-info/PKG-INFO
3
+ source/128Autograder.egg-info/SOURCES.txt
4
+ source/128Autograder.egg-info/dependency_links.txt
5
+ source/128Autograder.egg-info/entry_points.txt
6
+ source/128Autograder.egg-info/requires.txt
7
+ source/128Autograder.egg-info/top_level.txt
8
+ source/autograder_cli/__init__.py
9
+ source/autograder_cli/build_autograder.py
10
+ source/autograder_cli/create_upload.py
11
+ source/autograder_cli/run_gradescope.py
12
+ source/autograder_cli/run_local.py
13
+ source/autograder_cli/run_prairielearn.py
14
+ source/autograder_platform/__init__.py
15
+ source/autograder_platform/cli.py
16
+ source/autograder_platform/Executors/Environment.py
17
+ source/autograder_platform/Executors/Executor.py
18
+ source/autograder_platform/Executors/__init__.py
19
+ source/autograder_platform/Executors/common.py
20
+ source/autograder_platform/StudentSubmission/AbstractStudentSubmission.py
21
+ source/autograder_platform/StudentSubmission/AbstractValidator.py
22
+ source/autograder_platform/StudentSubmission/GenericValidators.py
23
+ source/autograder_platform/StudentSubmission/ISubmissionProcess.py
24
+ source/autograder_platform/StudentSubmission/SubmissionProcessFactory.py
25
+ source/autograder_platform/StudentSubmission/__init__.py
26
+ source/autograder_platform/StudentSubmission/common.py
27
+ source/autograder_platform/StudentSubmissionImpl/__init__.py
28
+ source/autograder_platform/StudentSubmissionImpl/Python/AbstractPythonImportFactory.py
29
+ source/autograder_platform/StudentSubmissionImpl/Python/PythonEnvironment.py
30
+ source/autograder_platform/StudentSubmissionImpl/Python/PythonFileImportFactory.py
31
+ source/autograder_platform/StudentSubmissionImpl/Python/PythonModuleMockImportFactory.py
32
+ source/autograder_platform/StudentSubmissionImpl/Python/PythonSubmission.py
33
+ source/autograder_platform/StudentSubmissionImpl/Python/PythonSubmissionProcess.py
34
+ source/autograder_platform/StudentSubmissionImpl/Python/PythonValidators.py
35
+ source/autograder_platform/StudentSubmissionImpl/Python/Runners.py
36
+ source/autograder_platform/StudentSubmissionImpl/Python/__init__.py
37
+ source/autograder_platform/StudentSubmissionImpl/Python/common.py
38
+ source/autograder_platform/Tasks/Task.py
39
+ source/autograder_platform/Tasks/TaskRunner.py
40
+ source/autograder_platform/Tasks/__init__.py
41
+ source/autograder_platform/Tasks/common.py
42
+ source/autograder_platform/TestingFramework/Assertions.py
43
+ source/autograder_platform/TestingFramework/SingleFunctionMock.py
44
+ source/autograder_platform/TestingFramework/__init__.py
45
+ source/autograder_platform/config/Config.py
46
+ source/autograder_platform/config/__init__.py
47
+ source/autograder_platform/config/common.py
@@ -0,0 +1,7 @@
1
+ [console_scripts]
2
+ build_autograder = autograder_cli.build_autograder:tool
3
+ create_gradescope_upload = autograder_cli.create_upload:tool
4
+ run_autograder = autograder_cli.run_local:tool
5
+ run_gradescope = autograder_cli.run_gradescope:tool
6
+ run_prairielearn = autograder_cli.run_prairielearn:tool
7
+ test_my_work = autograder_cli.run_local:tool
@@ -0,0 +1,10 @@
1
+ HybridJSONTestRunner==0.8.1
2
+ dill==0.3.6
3
+ Better-PyUnit-Format==0.2.3
4
+ schema==0.7.5
5
+ requests==2.31.0
6
+ tomli==2.0.1
7
+
8
+ [dev]
9
+ coverage[toml]
10
+ build
@@ -0,0 +1,2 @@
1
+ autograder_cli
2
+ autograder_platform
File without changes
@@ -0,0 +1,314 @@
1
+ import os
2
+ import re
3
+ import shutil
4
+ from enum import Enum
5
+ from typing import List, Dict, Callable
6
+
7
+ from autograder_platform.cli import AutograderCLITool
8
+ from autograder_platform.config.Config import AutograderConfigurationBuilder, AutograderConfiguration
9
+
10
+
11
+ class FilesEnum(Enum):
12
+ PUBLIC_TEST = 0
13
+ PRIVATE_TEST = 1
14
+ PUBLIC_DATA = 2
15
+ PRIVATE_DATA = 3
16
+ STARTER_CODE = 4
17
+ CONFIG_FILE = 5
18
+
19
+
20
+ class Build:
21
+ IGNORE = ["__pycache__"]
22
+
23
+ def __init__(self, config: AutograderConfiguration, sourceRoot, binRoot, version) -> None:
24
+ self.config = config
25
+ self.generationDirectory = os.path.join(binRoot, "generation")
26
+ self.distDirectory = os.path.join(binRoot, "dist")
27
+ self.binRoot = binRoot
28
+ self.sourceDir = sourceRoot
29
+ self.version = version
30
+
31
+ @staticmethod
32
+ def _discoverTestFiles(allowPrivate: bool,
33
+ privateTestFileRegex: re.Pattern, publicTestFileRegex: re.Pattern,
34
+ testDirectory: str,
35
+ discoveredPrivateFiles: List[str], discoveredPublicFiles: List[str]):
36
+ """
37
+ Description
38
+ ---
39
+
40
+ This function discovers the test files in the testDirectory and puts them in the correct list
41
+ based on the regex matches.
42
+
43
+ Note: If allowPrivate is false, then all files that match the private regex will be added to the public list
44
+
45
+ :param allowPrivate: If private files should be treated as private
46
+ :param privateTestFileRegex: The regex pattern used to match private test files
47
+ :param publicTestFileRegex: The regex pattern used to match public test files
48
+ :param discoveredPrivateFiles: The list that contains the private files to be copied
49
+ :param discoveredPublicFiles: The list that contains the public files to be copied
50
+ """
51
+
52
+ # test discovery is non recursive for now
53
+ test_files = [file for file in os.listdir(testDirectory) if os.path.isfile(os.path.join(testDirectory, file))]
54
+
55
+ for file in test_files:
56
+ path = os.path.join(testDirectory, file)
57
+ # Dont need to worry about double checking, the private test will only be run once in both of these cases
58
+ if allowPrivate and privateTestFileRegex.match(file):
59
+ discoveredPrivateFiles.append(path)
60
+ elif publicTestFileRegex.match(file) or privateTestFileRegex.match(file):
61
+ discoveredPublicFiles.append(path)
62
+
63
+ @staticmethod
64
+ def _discoverDataFiles(allowPrivate: bool, dataFilesSource: str,
65
+ discoveredPrivateFiles: List[str], discoveredPublicFiles: List[str]):
66
+ """
67
+ Description
68
+ ---
69
+
70
+ This function recursively discovers the data files in the dataFilesSource.
71
+ As opposed to the test file function, this will mark files as private if they contain 'private' anywhere in the path.
72
+
73
+ Note: if allowPrivate is false, then all files that would otherwise be private will be added to the public list
74
+
75
+ In the godforsaken event that some how we have a directory structure that exceeds 1000 folders, this will fail
76
+ due to a recursion error
77
+
78
+ :param allowPrivate: If private files should be treated as private
79
+ :param dataFilesSource: The current search directory
80
+ :param discoveredPrivateFiles: The list that contains the private files to be copied
81
+ :param discoveredPublicFiles: the list that contains the public files to be copied
82
+ """
83
+
84
+ for file in os.listdir(dataFilesSource):
85
+ # ignore hidden files + directories
86
+ if file[0] == ".":
87
+ continue
88
+
89
+ path = os.path.join(dataFilesSource, file)
90
+
91
+ # ignore any and all test files
92
+ if os.path.isfile(path) and "test" in file.lower():
93
+ continue
94
+
95
+ if os.path.isdir(path):
96
+ Build._discoverDataFiles(allowPrivate, path, discoveredPrivateFiles, discoveredPublicFiles)
97
+ continue
98
+
99
+ if allowPrivate and "private" in path.lower():
100
+ discoveredPrivateFiles.append(path)
101
+ continue
102
+
103
+ discoveredPublicFiles.append(path)
104
+
105
+ def discoverFiles(self) -> Dict[FilesEnum, List[str]]:
106
+ """
107
+ Description
108
+ ---
109
+
110
+ This function discovers all of the user defined files to copy.
111
+ See :ref:`_discoverTestFiles` and :ref:`_discoverDataFiles` for more information on how this process works
112
+
113
+ :returns: A map containing all the user defined files to copy
114
+ """
115
+ config = self.config.build
116
+
117
+ files: Dict[FilesEnum, List[str]] = {
118
+ FilesEnum.PUBLIC_TEST: [],
119
+ FilesEnum.PRIVATE_TEST: [],
120
+ FilesEnum.PUBLIC_DATA: [],
121
+ FilesEnum.PRIVATE_DATA: [],
122
+ FilesEnum.STARTER_CODE: [],
123
+ FilesEnum.CONFIG_FILE: [],
124
+ }
125
+
126
+ self._discoverTestFiles(config.allow_private,
127
+ re.compile(config.private_tests_regex), re.compile(config.public_tests_regex),
128
+ self.config.config.test_directory,
129
+ files[FilesEnum.PRIVATE_TEST], files[FilesEnum.PUBLIC_TEST])
130
+
131
+ # imo, this is not worth moving into its function atm
132
+ if config.use_starter_code:
133
+ # we can assume that the file exists if the config has it
134
+ files[FilesEnum.STARTER_CODE].append(config.starter_code_source)
135
+
136
+ if config.use_data_files:
137
+ self._discoverDataFiles(config.allow_private, config.data_files_source,
138
+ files[FilesEnum.PRIVATE_DATA], files[FilesEnum.PUBLIC_DATA])
139
+
140
+ files[FilesEnum.CONFIG_FILE] = [os.path.join(self.sourceDir, "config.toml")]
141
+
142
+ return files
143
+
144
+ @staticmethod
145
+ def copy(src, dest):
146
+ if os.path.isdir(src):
147
+ shutil.copytree(src, dest)
148
+ return
149
+
150
+ shutil.copy(src, dest)
151
+
152
+ def createFolders(self):
153
+ # clean build if it exists
154
+ if os.path.exists(self.binRoot):
155
+ try:
156
+ shutil.rmtree(self.binRoot, ignore_errors=True)
157
+ except OSError:
158
+ print("WARN: Failed to clean bin directory")
159
+
160
+ # create directories
161
+ os.makedirs(self.generationDirectory, exist_ok=True)
162
+ os.makedirs(self.distDirectory, exist_ok=True)
163
+
164
+ @staticmethod
165
+ def createSetupForGradescope(path: str, version: str):
166
+ with open(os.path.join(path, "setup.sh"), "w") as w:
167
+ w.write(
168
+ "apt-get install python3.11 -y\n"
169
+ "apt-get install python3-pip -y\n"
170
+ # "apt-get install -y libgbm-dev xvfb\n"
171
+ "pip3 install --upgrade pip\n"
172
+ f"pip3 install 128Autograder=={version}\n"
173
+ )
174
+
175
+ @staticmethod
176
+ def createRunFileForGradescope(path: str):
177
+ with open(os.path.join(path, "run_autograder"), "w") as w:
178
+ w.write(
179
+ "#!/bin/bash\n"
180
+ "pushd source > /dev/null || echo 'Autograder failed to open source'\n"
181
+ "run_gradescope\n"
182
+ "popd > /dev/null || true\n"
183
+ )
184
+
185
+ @staticmethod
186
+ def createRunForPrairieLearn(path: str):
187
+ with open(os.path.join(path, "run_autograder"), "w") as w:
188
+ w.write(
189
+ "#!/bin/bash\n"
190
+ "pushd source > /dev/null || echo 'Autograder failed to open source'\n"
191
+ "run_prairielearn\n"
192
+ "popd > /dev/null || true\n"
193
+ )
194
+
195
+ @staticmethod
196
+ def generateDocker(generationPath: str, platform, files: Dict[FilesEnum, List[str]], version: str,
197
+ setupFileGenerator: Callable[[str, str], None], runFileGenerator: Callable[[str], None]):
198
+ generationPath = os.path.join(generationPath, "docker", platform)
199
+ os.makedirs(generationPath, exist_ok=True)
200
+
201
+ setupFileGenerator(generationPath, version)
202
+ runFileGenerator(generationPath)
203
+
204
+ for key, listOfFiles in files.items():
205
+ if key is FilesEnum.STARTER_CODE:
206
+ continue
207
+
208
+ for file in listOfFiles:
209
+ destPath = os.path.join(generationPath, file)
210
+ os.makedirs(os.path.dirname(destPath), exist_ok=True)
211
+ Build.copy(file, destPath)
212
+
213
+ @staticmethod
214
+ def generateStudent(generationPath: str, files: Dict[FilesEnum, List[str]], studentWorkFolder: str):
215
+ generationPath = os.path.join(generationPath, "student")
216
+ os.makedirs(generationPath, exist_ok=True)
217
+
218
+ # create student_work folder
219
+ studentWorkFolder = os.path.join(generationPath, studentWorkFolder)
220
+ os.makedirs(studentWorkFolder, exist_ok=True)
221
+
222
+ for file in files[FilesEnum.PUBLIC_TEST]:
223
+ destPath = os.path.join(generationPath, file)
224
+ os.makedirs(os.path.dirname(destPath), exist_ok=True)
225
+ Build.copy(file, destPath)
226
+
227
+ for file in files[FilesEnum.PUBLIC_DATA]:
228
+ destPath = os.path.join(generationPath, file)
229
+ os.makedirs(os.path.dirname(destPath), exist_ok=True)
230
+ Build.copy(file, destPath)
231
+ # also add to student work folder
232
+ destPath = os.path.join(studentWorkFolder, os.path.basename(file))
233
+ Build.copy(file, destPath)
234
+
235
+ for file in files[FilesEnum.STARTER_CODE]:
236
+ destPath = os.path.join(studentWorkFolder, os.path.basename(file))
237
+ Build.copy(file, destPath)
238
+
239
+ for file in files[FilesEnum.CONFIG_FILE]:
240
+ destPath = os.path.join(generationPath, os.path.basename(file))
241
+ Build.copy(file, destPath)
242
+
243
+ # create .keep so that we dont loose the file
244
+ with open(os.path.join(studentWorkFolder, ".keep"), "w") as w:
245
+ w.write("DO NOT WRITE YOUR CODE HERE!\nCreate a *new* file in this directory!!!")
246
+
247
+ @staticmethod
248
+ def createDist(distType: str, generationPath: str, distPath: str, assignmentName: str):
249
+ generationPath = os.path.join(generationPath, distType)
250
+ if not os.path.exists(generationPath) or not os.path.isdir(generationPath):
251
+ raise AttributeError(f"Invalid generation path: {generationPath}")
252
+
253
+ os.makedirs(distPath, exist_ok=True)
254
+
255
+ assignmentName += f"-{'-'.join(distType.split('/'))}"
256
+ distPath = os.path.join(distPath, assignmentName)
257
+
258
+ shutil.make_archive(distPath, "zip", root_dir=generationPath)
259
+
260
+ def build(self):
261
+ files = self.discoverFiles()
262
+
263
+ self.createFolders()
264
+
265
+ if self.config.build.build_gradescope:
266
+ self.generateDocker(self.generationDirectory, "gradescope", files, self.version,
267
+ self.createSetupForGradescope, self.createRunFileForGradescope)
268
+ self.createDist("docker/gradescope", self.generationDirectory, self.distDirectory,
269
+ f"{self.config.semester}_{self.config.assignment_name}")
270
+ if self.config.build.build_prairie_learn:
271
+ # this build is a touch bigger than it needs to be, but for now I'm not super concerned about it
272
+ self.generateDocker(self.generationDirectory, "prairielearn", files, self.version,
273
+ lambda _, __: None, self.createRunForPrairieLearn)
274
+ self.createDist("docker/prairielearn", self.generationDirectory, self.distDirectory,
275
+ f"{self.config.semester}_{self.config.assignment_name}")
276
+
277
+ if self.config.build.build_student:
278
+ self.generateStudent(self.generationDirectory, files,
279
+ self.config.build.student_work_folder)
280
+ self.createDist("student", self.generationDirectory, self.distDirectory,
281
+ f"{self.config.semester}_{self.config.assignment_name}")
282
+
283
+
284
+ class BuildAutograderCLI(AutograderCLITool):
285
+ def __init__(self):
286
+ super().__init__(f"Builder v{AutograderCLITool.get_version()}")
287
+
288
+ def configure_options(self):
289
+ self.parser.add_argument("--source", default=".", help="Autograder source root")
290
+ self.parser.add_argument("-o", default="./bin", help="Output folder")
291
+
292
+ def run(self) -> bool:
293
+ self.configure_options()
294
+ self.load_config()
295
+
296
+ build = Build(self.config, self.arguments.source, self.arguments.o, self.get_version())
297
+
298
+ build.build()
299
+
300
+ return True
301
+
302
+ def set_config_arguments(self, configBuilder: AutograderConfigurationBuilder[AutograderConfiguration]):
303
+ pass
304
+
305
+
306
+ tool = BuildAutograderCLI().run
307
+
308
+ if __name__ == "__main__":
309
+ res = tool()
310
+
311
+ if res:
312
+ exit(0)
313
+
314
+ exit(1)
@@ -0,0 +1,103 @@
1
+ import getpass
2
+ import os
3
+ import sys
4
+ from zipfile import ZipFile
5
+
6
+
7
+ def buildZipName() -> str:
8
+ """
9
+ This function generates the zip file name and sets a fallback if the user's name can't be determined
10
+ :return: the generated zip file name
11
+ """
12
+ DEFAULT_NAME: str = "csci_128_student"
13
+ zipName: str = ""
14
+
15
+ try:
16
+ # Get user can throw an exception if it fails to look up the username in the password database
17
+ userName = getpass.getuser().lower()
18
+ # replace spaces with underscore bc spaces make me sad
19
+ userName = userName.replace(" ", "_")
20
+
21
+ zipName += userName
22
+
23
+ except KeyError:
24
+ print(f"Can't automatically determine user name. Defaulting to {DEFAULT_NAME}")
25
+ zipName += DEFAULT_NAME
26
+
27
+ zipName += "-submission"
28
+ zipName += ".zip"
29
+
30
+ return zipName
31
+
32
+
33
+ def addFolderToZip(_currentZipBuffer: ZipFile, _directoryToAdd: str) -> None:
34
+ """
35
+ This function allows us to recursively descend through the directory structure to
36
+ collect all of a student's submitted files
37
+ :param _currentZipBuffer: the zip file that is currently being written.
38
+ :param _directoryToAdd: the directory that needs to be added
39
+ """
40
+
41
+ # Ignore hidden directories
42
+ if _directoryToAdd[0] == ".":
43
+ return
44
+
45
+ # Ignore directories python directories
46
+ if "__" in _directoryToAdd:
47
+ return
48
+
49
+ print(f"\tEntering {_directoryToAdd}...")
50
+
51
+ for file in os.listdir(_directoryToAdd):
52
+ if os.path.isfile(_directoryToAdd + file) and file[-3:] == ".py":
53
+ print(f"\tAdding {_directoryToAdd + file}...")
54
+ _currentZipBuffer.write(_directoryToAdd + file)
55
+ elif os.path.isfile(_directoryToAdd + file):
56
+ print(f"\tIgnoring {_directoryToAdd + file}...")
57
+ else:
58
+ addFolderToZip(_currentZipBuffer, _directoryToAdd + file + "/")
59
+
60
+
61
+ def generateZipFile(_submissionDirectory: str) -> None:
62
+ """
63
+ This function generates the zip file needed for gradescope.
64
+ :param _submissionDirectory: the directory that the students work is in
65
+ """
66
+ GREEN_COLOR: str = u"\u001b[32m"
67
+ RESET_COLOR: str = u"\u001b[0m"
68
+
69
+ print("Generating gradescope upload...")
70
+
71
+ zipName: str = buildZipName()
72
+
73
+ with ZipFile(zipName, 'w') as submissionZip:
74
+ os.chdir(_submissionDirectory)
75
+ for file in os.listdir("."):
76
+ if os.path.isfile(file) and file[-3:] == ".py":
77
+ print(f"\tAdding {_submissionDirectory + file} to zip...")
78
+ submissionZip.write(file)
79
+ elif os.path.isfile(file):
80
+ print(f"\tIgnoring {_submissionDirectory + file}...")
81
+ else:
82
+ addFolderToZip(submissionZip, file + "/")
83
+
84
+ print("...Done.")
85
+ print(f"\n\n{GREEN_COLOR}Submit {zipName} to Gradescope under the corresponding assignment.{RESET_COLOR}")
86
+
87
+
88
+
89
+ def tool(): # pragma: no cover
90
+ submissionDirectory = "student_work"
91
+
92
+ if len(sys.argv) == 2:
93
+ submissionDirectory = sys.argv[1]
94
+
95
+ # need to make sure to that we have a / at the end of the path
96
+ if submissionDirectory[-1:] != '/':
97
+ submissionDirectory += "/"
98
+
99
+ generateZipFile(submissionDirectory)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ tool()