QuizGenerator 0.4.2__py3-none-any.whl → 0.6.0__py3-none-any.whl

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 (33) hide show
  1. QuizGenerator/contentast.py +809 -117
  2. QuizGenerator/generate.py +219 -11
  3. QuizGenerator/misc.py +0 -556
  4. QuizGenerator/mixins.py +50 -29
  5. QuizGenerator/premade_questions/basic.py +3 -3
  6. QuizGenerator/premade_questions/cst334/languages.py +183 -175
  7. QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
  8. QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
  9. QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
  10. QuizGenerator/premade_questions/cst334/process.py +558 -79
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
  12. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
  13. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
  14. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
  15. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
  16. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
  17. QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
  18. QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
  19. QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
  20. QuizGenerator/premade_questions/cst463/models/text.py +29 -15
  21. QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
  22. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
  23. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
  24. QuizGenerator/question.py +114 -20
  25. QuizGenerator/quiz.py +81 -24
  26. QuizGenerator/regenerate.py +98 -29
  27. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
  28. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
  29. QuizGenerator/README.md +0 -5
  30. QuizGenerator/logging.yaml +0 -55
  31. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
  32. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
  33. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/generate.py CHANGED
@@ -1,15 +1,19 @@
1
1
  #!env python
2
2
  import argparse
3
+ from datetime import datetime
3
4
  import os
4
5
  import random
5
6
  import shutil
6
7
  import subprocess
7
8
  import tempfile
9
+ import traceback
10
+ import re
8
11
  from pathlib import Path
9
12
  from dotenv import load_dotenv
10
13
  from QuizGenerator.canvas.canvas_interface import CanvasInterface
11
14
 
12
15
  from QuizGenerator.quiz import Quiz
16
+ from QuizGenerator.question import QuestionRegistry
13
17
 
14
18
  import logging
15
19
  log = logging.getLogger(__name__)
@@ -41,8 +45,13 @@ def parse_args():
41
45
 
42
46
  # PDF Flags
43
47
  parser.add_argument("--num_pdfs", default=0, type=int, help="How many PDF quizzes to create")
44
- parser.add_argument("--typst", action="store_true",
45
- help="Use Typst instead of LaTeX for PDF generation")
48
+ parser.add_argument("--latex", action="store_false", dest="typst", help="Use Typst instead of LaTeX for PDF generation")
49
+
50
+ # Testing flags
51
+ parser.add_argument("--test_all", type=int, default=0, metavar="N",
52
+ help="Generate N variations of ALL registered questions to test they work correctly")
53
+ parser.add_argument("--strict", action="store_true",
54
+ help="With --test_all, skip PDF/Canvas generation if any questions fail")
46
55
 
47
56
  subparsers = parser.add_subparsers(dest='command')
48
57
  test_parser = subparsers.add_parser("TEST")
@@ -63,14 +72,169 @@ def test():
63
72
  print("\n" + "="*60)
64
73
  print("TEST COMPLETE")
65
74
  print("="*60)
66
-
67
-
68
- def generate_latex(latex_text, remove_previous=False):
69
75
 
76
+
77
+ def test_all_questions(
78
+ num_variations: int,
79
+ generate_pdf: bool = False,
80
+ use_typst: bool = True,
81
+ canvas_course=None,
82
+ strict: bool = False
83
+ ):
84
+ """
85
+ Test all registered questions by generating N variations of each.
86
+
87
+ This helps verify that all question types work correctly and can generate
88
+ valid output without errors.
89
+
90
+ Args:
91
+ num_variations: Number of variations to generate for each question type
92
+ generate_pdf: If True, generate a PDF with all successful questions
93
+ use_typst: If True, use Typst for PDF generation; otherwise use LaTeX
94
+ canvas_course: If provided, push a test quiz to this Canvas course
95
+ strict: If True, skip PDF/Canvas generation if any questions fail
96
+ """
97
+ # Ensure all premade questions are loaded
98
+ QuestionRegistry.load_premade_questions()
99
+
100
+ registered_questions = QuestionRegistry._registry
101
+ total_questions = len(registered_questions)
102
+
103
+ # Test defaults for questions that require external input
104
+ # These are "template" questions that can't work without content
105
+ TEST_DEFAULTS = {
106
+ 'fromtext': {'text': 'Test question placeholder text.'},
107
+ 'fromgenerator': {'generator': 'return "Generated test content"'},
108
+ }
109
+
110
+ print(f"\nTesting {total_questions} registered question types with {num_variations} variations each...")
111
+ print("=" * 70)
112
+
113
+ failed_questions = []
114
+ successful_questions = []
115
+ # Collect question instances for PDF/Canvas generation
116
+ test_question_instances = []
117
+
118
+ for i, (question_name, question_class) in enumerate(sorted(registered_questions.items()), 1):
119
+ print(f"\n[{i}/{total_questions}] Testing: {question_name}")
120
+ print(f" Class: {question_class.__name__}")
121
+
122
+ question_failures = []
123
+
124
+ for variation in range(num_variations):
125
+ seed = variation * 1000 # Use different seeds for each variation
126
+ try:
127
+ # Get any test defaults for this question type
128
+ extra_kwargs = TEST_DEFAULTS.get(question_name, {})
129
+
130
+ # Create question instance with minimal required params
131
+ question = question_class(
132
+ name=f"{question_name} (v{variation+1})",
133
+ points_value=1.0,
134
+ **extra_kwargs
135
+ )
136
+
137
+ # Generate the question (this calls refresh and builds the AST)
138
+ question_ast = question.get_question(rng_seed=seed)
139
+
140
+ # Try rendering to both formats to catch format-specific issues
141
+ try:
142
+ question_ast.render("html")
143
+ except Exception as e:
144
+ tb = traceback.format_exc()
145
+ question_failures.append(f" Variation {variation+1}: HTML render failed - {e}\n{tb}")
146
+ continue
147
+
148
+ try:
149
+ question_ast.render("typst")
150
+ except Exception as e:
151
+ tb = traceback.format_exc()
152
+ question_failures.append(f" Variation {variation+1}: Typst render failed - {e}\n{tb}")
153
+ continue
154
+
155
+ # If we got here, the question works - save the instance
156
+ test_question_instances.append(question)
157
+
158
+ except Exception as e:
159
+ tb = traceback.format_exc()
160
+ question_failures.append(f" Variation {variation+1}: Generation failed - {e}\n{tb}")
161
+
162
+ if question_failures:
163
+ print(f" FAILED ({len(question_failures)}/{num_variations} variations)")
164
+ for failure in question_failures:
165
+ print(failure)
166
+ failed_questions.append((question_name, question_failures))
167
+ else:
168
+ print(f" OK ({num_variations}/{num_variations} variations)")
169
+ successful_questions.append(question_name)
170
+
171
+ # Summary
172
+ print("\n" + "=" * 70)
173
+ print("TEST SUMMARY")
174
+ print("=" * 70)
175
+ print(f"Total question types: {total_questions}")
176
+ print(f"Successful: {len(successful_questions)}")
177
+ print(f"Failed: {len(failed_questions)}")
178
+
179
+ if failed_questions:
180
+ print("\nFailed questions:")
181
+ for name, failures in failed_questions:
182
+ print(f" - {name}: {len(failures)} failures")
183
+
184
+ print("=" * 70)
185
+
186
+ # Generate PDF and/or push to Canvas if requested
187
+ if strict and failed_questions:
188
+ print("\n[STRICT MODE] Skipping PDF/Canvas generation due to failures")
189
+ elif (generate_pdf or canvas_course) and test_question_instances:
190
+ print(f"\nCreating test quiz with {len(test_question_instances)} questions...")
191
+
192
+ # Create a Quiz object with all successful questions
193
+ test_quiz = Quiz(
194
+ name="Test All Questions",
195
+ questions=test_question_instances,
196
+ practice=True
197
+ )
198
+
199
+ if generate_pdf:
200
+ print("Generating PDF...")
201
+ pdf_seed = 12345 # Fixed seed for reproducibility
202
+ if use_typst:
203
+ typst_text = test_quiz.get_quiz(rng_seed=pdf_seed).render("typst")
204
+ generate_typst(typst_text, remove_previous=True, name_prefix="test_all_questions")
205
+ else:
206
+ latex_text = test_quiz.get_quiz(rng_seed=pdf_seed).render_latex()
207
+ generate_latex(latex_text, remove_previous=True, name_prefix="test_all_questions")
208
+ print("PDF generated in out/ directory")
209
+
210
+ if canvas_course:
211
+ print("Pushing to Canvas...")
212
+ quiz_title = f"Test All Questions ({int(datetime.now().timestamp())} : {datetime.now().strftime('%b %d %I:%M%p')})"
213
+ canvas_course.push_quiz_to_canvas(
214
+ test_quiz,
215
+ num_variations=1,
216
+ title=quiz_title,
217
+ is_practice=True
218
+ )
219
+ print(f"Quiz '{quiz_title}' pushed to Canvas")
220
+
221
+ return len(failed_questions) == 0
222
+
223
+
224
+ def generate_latex(latex_text, remove_previous=False, name_prefix=None):
225
+ """
226
+ Generate PDF from LaTeX source code.
227
+
228
+ Args:
229
+ latex_text: The LaTeX source code to compile
230
+ remove_previous: Whether to remove the 'out' directory before generating
231
+ name_prefix: Optional prefix for the temporary filename (e.g., quiz name)
232
+ """
70
233
  if remove_previous:
71
234
  if os.path.exists('out'): shutil.rmtree('out')
72
235
 
73
- tmp_tex = tempfile.NamedTemporaryFile('w')
236
+ prefix = f"{sanitize_filename(name_prefix)}-" if name_prefix else "tmp"
237
+ tmp_tex = tempfile.NamedTemporaryFile('w', prefix=prefix)
74
238
 
75
239
  tmp_tex.write(latex_text)
76
240
 
@@ -98,11 +262,38 @@ def generate_latex(latex_text, remove_previous=False):
98
262
  tmp_tex.close()
99
263
 
100
264
 
101
- def generate_typst(typst_text, remove_previous=False):
265
+ def sanitize_filename(name):
266
+ """
267
+ Sanitize a quiz name for use as a filename prefix.
268
+
269
+ Converts spaces to underscores, removes special characters,
270
+ and limits length to avoid overly long filenames.
271
+
272
+ Example: "CST 334 Exam 4 (Fall 25)" -> "CST_334_Exam_4_Fall_25"
273
+ """
274
+ # Replace spaces with underscores
275
+ sanitized = name.replace(' ', '_')
276
+
277
+ # Remove characters that aren't alphanumeric, underscore, or hyphen
278
+ sanitized = re.sub(r'[^\w\-]', '', sanitized)
279
+
280
+ # Limit length to avoid overly long filenames (keep first 50 chars)
281
+ if len(sanitized) > 50:
282
+ sanitized = sanitized[:50]
283
+
284
+ return sanitized
285
+
286
+
287
+ def generate_typst(typst_text, remove_previous=False, name_prefix=None):
102
288
  """
103
289
  Generate PDF from Typst source code.
104
290
 
105
291
  Similar to generate_latex, but uses typst compiler instead of latexmk.
292
+
293
+ Args:
294
+ typst_text: The Typst source code to compile
295
+ remove_previous: Whether to remove the 'out' directory before generating
296
+ name_prefix: Optional prefix for the temporary filename (e.g., quiz name)
106
297
  """
107
298
  if remove_previous:
108
299
  if os.path.exists('out'):
@@ -111,8 +302,9 @@ def generate_typst(typst_text, remove_previous=False):
111
302
  # Ensure output directory exists
112
303
  os.makedirs('out', exist_ok=True)
113
304
 
114
- # Create temporary Typst file
115
- tmp_typ = tempfile.NamedTemporaryFile('w', suffix='.typ', delete=False)
305
+ # Create temporary Typst file with optional name prefix
306
+ prefix = f"{sanitize_filename(name_prefix)}-" if name_prefix else "tmp"
307
+ tmp_typ = tempfile.NamedTemporaryFile('w', suffix='.typ', delete=False, prefix=prefix)
116
308
 
117
309
  try:
118
310
  tmp_typ.write(typst_text)
@@ -190,11 +382,11 @@ def generate_quiz(
190
382
  if use_typst:
191
383
  # Generate using Typst
192
384
  typst_text = quiz.get_quiz(rng_seed=pdf_seed, use_typst_measurement=use_typst_measurement).render("typst")
193
- generate_typst(typst_text, remove_previous=(i==0))
385
+ generate_typst(typst_text, remove_previous=(i==0), name_prefix=quiz.name)
194
386
  else:
195
387
  # Generate using LaTeX (default)
196
388
  latex_text = quiz.get_quiz(rng_seed=pdf_seed, use_typst_measurement=use_typst_measurement).render_latex()
197
- generate_latex(latex_text, remove_previous=(i==0))
389
+ generate_latex(latex_text, remove_previous=(i==0), name_prefix=quiz.name)
198
390
 
199
391
  if num_canvas > 0:
200
392
  canvas_course.push_quiz_to_canvas(
@@ -233,6 +425,22 @@ def main():
233
425
  test()
234
426
  return
235
427
 
428
+ if args.test_all > 0:
429
+ # Set up Canvas course if course_id provided
430
+ canvas_course = None
431
+ if args.course_id:
432
+ canvas_interface = CanvasInterface(prod=args.prod)
433
+ canvas_course = canvas_interface.get_course(course_id=args.course_id)
434
+
435
+ success = test_all_questions(
436
+ args.test_all,
437
+ generate_pdf=True,
438
+ use_typst=getattr(args, 'typst', True),
439
+ canvas_course=canvas_course,
440
+ strict=args.strict
441
+ )
442
+ exit(0 if success else 1)
443
+
236
444
  # Clear any previous metrics
237
445
  PerformanceTracker.clear_metrics()
238
446