QuizGenerator 0.7.0__py3-none-any.whl → 0.8.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 (30) hide show
  1. QuizGenerator/contentast.py +6 -6
  2. QuizGenerator/generate.py +2 -1
  3. QuizGenerator/mixins.py +14 -100
  4. QuizGenerator/premade_questions/basic.py +24 -29
  5. QuizGenerator/premade_questions/cst334/languages.py +100 -99
  6. QuizGenerator/premade_questions/cst334/math_questions.py +112 -122
  7. QuizGenerator/premade_questions/cst334/memory_questions.py +621 -621
  8. QuizGenerator/premade_questions/cst334/persistence_questions.py +137 -163
  9. QuizGenerator/premade_questions/cst334/process.py +312 -322
  10. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +34 -35
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +41 -36
  12. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +48 -41
  13. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +285 -520
  14. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +149 -126
  15. QuizGenerator/premade_questions/cst463/models/attention.py +44 -50
  16. QuizGenerator/premade_questions/cst463/models/cnns.py +43 -47
  17. QuizGenerator/premade_questions/cst463/models/matrices.py +61 -11
  18. QuizGenerator/premade_questions/cst463/models/rnns.py +48 -50
  19. QuizGenerator/premade_questions/cst463/models/text.py +65 -67
  20. QuizGenerator/premade_questions/cst463/models/weight_counting.py +47 -46
  21. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +100 -156
  22. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +93 -141
  23. QuizGenerator/question.py +273 -202
  24. QuizGenerator/quiz.py +8 -5
  25. QuizGenerator/regenerate.py +128 -19
  26. {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/METADATA +30 -2
  27. {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/RECORD +30 -30
  28. {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/WHEEL +0 -0
  29. {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/entry_points.txt +0 -0
  30. {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -651,24 +651,24 @@ class Question(Container):
651
651
 
652
652
  class Section(Container):
653
653
  """
654
- Primary container for question content - USE THIS for get_body() and get_explanation().
654
+ Primary container for question content - USE THIS for _build_body() and _build_explanation().
655
655
 
656
656
  This is the most important content AST class for question developers.
657
657
  It serves as the main container for organizing question content
658
- and should be the return type for your get_body() and get_explanation() methods.
658
+ and should be the return type for your _build_body() and _build_explanation() methods.
659
659
 
660
660
  CRITICAL: Always use Section as the container for:
661
- - Question body content (return from get_body())
662
- - Question explanation/solution content (return from get_explanation())
661
+ - Question body content (return from _build_body())
662
+ - Question explanation/solution content (return from _build_explanation())
663
663
  - Any grouped content that needs to render together
664
664
 
665
665
  When to use:
666
- - As the root container in get_body() and get_explanation() methods
666
+ - As the root container in _build_body() and _build_explanation() methods
667
667
  - Grouping related content elements
668
668
  - Organizing complex question content
669
669
 
670
670
  Example:
671
- def _get_body(self):
671
+ def _build_body(self, context):
672
672
  body = Section()
673
673
  answers = []
674
674
  body.add_element(Paragraph(["Calculate the determinant:"]))
QuizGenerator/generate.py CHANGED
@@ -153,7 +153,8 @@ def test_all_questions(
153
153
  )
154
154
 
155
155
  # Generate the question (this calls refresh and builds the AST)
156
- question_ast = question.get_question(rng_seed=seed)
156
+ instance = question.instantiate(rng_seed=seed)
157
+ question_ast = question._build_question_ast(instance)
157
158
 
158
159
  # Try rendering to both formats to catch format-specific issues
159
160
  try:
QuizGenerator/mixins.py CHANGED
@@ -17,7 +17,8 @@ class TableQuestionMixin:
17
17
  across question types, reducing repetitive ca.Table creation code.
18
18
  """
19
19
 
20
- def create_info_table(self, info_dict: Dict[str, Any], transpose: bool = False) -> ca.Table:
20
+ @staticmethod
21
+ def create_info_table(info_dict: Dict[str, Any], transpose: bool = False) -> ca.Table:
21
22
  """
22
23
  Creates a vertical info table (key-value pairs).
23
24
 
@@ -45,8 +46,8 @@ class TableQuestionMixin:
45
46
  transpose=transpose
46
47
  )
47
48
 
49
+ @staticmethod
48
50
  def create_answer_table(
49
- self,
50
51
  headers: List[str],
51
52
  data_rows: List[Dict[str, Any]],
52
53
  answer_columns: List[str] = None
@@ -75,11 +76,6 @@ class TableQuestionMixin:
75
76
  # Answer extends ca.Leaf, so it can be used directly
76
77
  if column in answer_columns and isinstance(value, ca.Answer):
77
78
  return value
78
- # If this column should contain answers but we have the answer key
79
- elif column in answer_columns and isinstance(value, str) and hasattr(self, 'answers'):
80
- answer_obj = self.answers.get(value)
81
- if answer_obj:
82
- return answer_obj
83
79
 
84
80
  # Otherwise return as plain data
85
81
  return str(value)
@@ -94,11 +90,11 @@ class TableQuestionMixin:
94
90
  data=table_data
95
91
  )
96
92
 
93
+ @staticmethod
97
94
  def create_parameter_answer_table(
98
- self,
99
95
  parameter_info: Dict[str, Any],
100
96
  answer_label: str,
101
- answer_key: str,
97
+ answer: ca.Answer,
102
98
  transpose: bool = True
103
99
  ) -> ca.Table:
104
100
  """
@@ -110,7 +106,7 @@ class TableQuestionMixin:
110
106
  Args:
111
107
  parameter_info: Dictionary of {parameter_name: value}
112
108
  answer_label: Label for the answer row
113
- answer_key: Key to look up the answer in self.answers
109
+ answer: Answer object to embed in the table
114
110
  transpose: Whether to show as vertical table (default: True)
115
111
 
116
112
  Returns:
@@ -120,18 +116,15 @@ class TableQuestionMixin:
120
116
  data = [[key, str(value)] for key, value in parameter_info.items()]
121
117
 
122
118
  # Add answer row - Answer extends ca.Leaf so it can be used directly
123
- if hasattr(self, 'answers') and answer_key in self.answers:
124
- data.append([answer_label, self.answers[answer_key]])
125
- else:
126
- data.append([answer_label, f"[{answer_key}]"]) # Fallback
119
+ data.append([answer_label, answer])
127
120
 
128
121
  return ca.Table(
129
122
  data=data,
130
123
  transpose=transpose
131
124
  )
132
125
 
126
+ @staticmethod
133
127
  def create_fill_in_table(
134
- self,
135
128
  headers: List[str],
136
129
  template_rows: List[Dict[str, Any]]
137
130
  ) -> ca.Table:
@@ -155,11 +148,6 @@ class TableQuestionMixin:
155
148
  # Answer extends ca.Leaf so it can be used in the AST
156
149
  if isinstance(value, ca.Answer):
157
150
  return value
158
- # If it's a string that looks like an answer key, try to resolve it
159
- elif isinstance(value, str) and value.startswith("answer__") and hasattr(self, 'answers'):
160
- answer_obj = self.answers.get(value)
161
- if answer_obj:
162
- return answer_obj
163
151
  # Otherwise return as-is
164
152
  return str(value)
165
153
 
@@ -182,8 +170,8 @@ class BodyTemplatesMixin:
182
170
  common question layout patterns.
183
171
  """
184
172
 
173
+ @staticmethod
185
174
  def create_calculation_with_info_body(
186
- self,
187
175
  intro_text: str,
188
176
  info_table: ca.Table,
189
177
  answer_block: ca.AnswerBlock
@@ -199,8 +187,8 @@ class BodyTemplatesMixin:
199
187
  body.add_element(answer_block)
200
188
  return body
201
189
 
190
+ @staticmethod
202
191
  def create_fill_in_table_body(
203
- self,
204
192
  intro_text: str,
205
193
  instructions: str,
206
194
  table: ca.Table
@@ -218,8 +206,8 @@ class BodyTemplatesMixin:
218
206
  body.add_element(table)
219
207
  return body
220
208
 
209
+ @staticmethod
221
210
  def create_parameter_calculation_body(
222
- self,
223
211
  intro_text: str,
224
212
  parameter_table: ca.Table,
225
213
  answer_table: ca.Table = None,
@@ -357,35 +345,6 @@ class MultiPartQuestionMixin:
357
345
 
358
346
  return body
359
347
 
360
- def get_subpart_answers(self):
361
- """
362
- Retrieve answers organized by subpart for multipart questions.
363
-
364
- Returns:
365
- dict: Dictionary mapping subpart letters ('a', 'b', 'c') to their answers.
366
- Returns empty dict if not a multipart question.
367
-
368
- Example:
369
- # For a 3-part question
370
- {
371
- 'a': ca.Answer.integer('a', 5),
372
- 'b': ca.Answer.integer('b', 12),
373
- 'c': ca.Answer.integer('c', -3)
374
- }
375
- """
376
- if not self.is_multipart():
377
- return {}
378
-
379
- subpart_answers = {}
380
- for i in range(self.num_subquestions):
381
- letter = chr(ord('a') + i)
382
- # Look for answers with subpart keys
383
- answer_key = f"subpart_{letter}"
384
- if hasattr(self, 'answers') and answer_key in self.answers:
385
- subpart_answers[letter] = self.answers[answer_key]
386
-
387
- return subpart_answers
388
-
389
348
 
390
349
  class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
391
350
  """
@@ -450,40 +409,6 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
450
409
  """Create answers for single questions - just delegate to subquestion method."""
451
410
  return self.create_subquestion_answers(0, result)
452
411
 
453
- def refresh(self, *args, **kwargs):
454
- super().refresh(*args, **kwargs)
455
-
456
- # Clear any existing data
457
- self.answers = {}
458
-
459
- if self.is_multipart():
460
- # Generate multiple subquestions
461
- self.subquestion_data = []
462
- for i in range(self.num_subquestions):
463
- # Generate unique operands for each subquestion
464
- operand_a, operand_b = self.generate_operands()
465
- result = self.calculate_single_result(operand_a, operand_b)
466
-
467
- self.subquestion_data.append(
468
- {
469
- 'operand_a': operand_a,
470
- 'operand_b': operand_b,
471
- 'vector_a': operand_a, # For vector compatibility
472
- 'vector_b': operand_b, # For vector compatibility
473
- 'result': result
474
- }
475
- )
476
-
477
- # Create answers for this subpart
478
- self.create_subquestion_answers(i, result)
479
- else:
480
- # Single question (original behavior)
481
- self.operand_a, self.operand_b = self.generate_operands()
482
- self.result = self.calculate_single_result(self.operand_a, self.operand_b)
483
-
484
- # Create answers
485
- self.create_single_answers(self.result)
486
-
487
412
  def generate_subquestion_data(self):
488
413
  """Generate LaTeX content for each subpart of the question."""
489
414
  subparts = []
@@ -494,7 +419,7 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
494
419
  subparts.append((operand_a_latex, self.get_operator(), operand_b_latex))
495
420
  return subparts
496
421
 
497
- def _get_body(self):
422
+ def _build_body(self, context):
498
423
  """Build question body and collect answers."""
499
424
  body = ca.Section()
500
425
  answers = []
@@ -506,8 +431,7 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
506
431
  subpart_data = self.generate_subquestion_data()
507
432
  repeated_part = self.create_repeated_problem_part(subpart_data)
508
433
  body.add_element(repeated_part)
509
- # Collect answers from self.answers dict
510
- answers = list(self.answers.values())
434
+ answers = list(self._generated_answers)
511
435
  else:
512
436
  # Single equation display
513
437
  equation_latex = self.format_single_equation(self.operand_a, self.operand_b)
@@ -520,11 +444,6 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
520
444
 
521
445
  return body, answers
522
446
 
523
- def get_body(self):
524
- """Build question body (backward compatible interface)."""
525
- body, _ = self._get_body()
526
- return body
527
-
528
447
  def _add_single_question_answers(self, body):
529
448
  """Add Canvas-only answer fields for single questions. Subclasses can override.
530
449
 
@@ -534,7 +453,7 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
534
453
  # Default implementation - subclasses should override for specific answer formats
535
454
  return []
536
455
 
537
- def _get_explanation(self):
456
+ def _build_explanation(self, context):
538
457
  """Default explanation structure. Subclasses should override for specific explanations."""
539
458
  explanation = ca.Section()
540
459
 
@@ -551,11 +470,6 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
551
470
 
552
471
  return explanation, []
553
472
 
554
- def get_explanation(self):
555
- """Build question explanation (backward compatible interface)."""
556
- explanation, _ = self._get_explanation()
557
- return explanation
558
-
559
473
  def get_explanation_intro(self):
560
474
  """Get the intro text for explanations. Subclasses should override."""
561
475
  return "The calculation is performed as follows:"
@@ -1,7 +1,8 @@
1
1
  #!env python
2
2
  from __future__ import annotations
3
3
 
4
- from typing import List, Dict, Any, Tuple
4
+ from typing import Tuple, List
5
+ import random
5
6
 
6
7
  import logging
7
8
 
@@ -18,15 +19,18 @@ class FromText(Question):
18
19
  def __init__(self, *args, text, **kwargs):
19
20
  super().__init__(*args, **kwargs)
20
21
  self.text = text
21
- self.answers = []
22
22
  self.possible_variations = 1
23
23
 
24
- def get_body(self, **kwargs) -> ca.Section:
25
-
26
- return ca.Section([ca.Text(self.text)])
24
+ def _build_context(self, *, rng_seed=None, **kwargs):
25
+ context = super()._build_context(rng_seed=rng_seed, **kwargs)
26
+ context["text"] = self.text
27
+ return context
27
28
 
28
- def get_answers(self, *args, **kwargs) -> Tuple[ca.Answer.CanvasAnswerKind, List[Dict[str,Any]]]:
29
- return ca.Answer.CanvasAnswerKind.ESSAY, []
29
+ def _build_body(self, context) -> Tuple[ca.Element, List[ca.Answer]]:
30
+ return ca.Section([ca.Text(context["text"])]), []
31
+
32
+ def _build_explanation(self, context) -> Tuple[ca.Element, List[ca.Answer]]:
33
+ return ca.Section(), []
30
34
 
31
35
 
32
36
  @QuestionRegistry.register()
@@ -43,7 +47,8 @@ class FromGenerator(FromText, TableQuestionMixin):
43
47
  self.possible_variations = kwargs.get("possible_variations", float('inf'))
44
48
 
45
49
  def attach_function_to_object(obj, function_code, function_name='get_body_lines'):
46
- function_code = "import random\n" + function_code
50
+ # Provide a deterministic RNG handle for generator snippets.
51
+ function_code = "rng = self.rng\n" + function_code
47
52
 
48
53
  # Create a local namespace for exec with content AST helpers available
49
54
  local_namespace = {
@@ -66,38 +71,28 @@ class FromGenerator(FromText, TableQuestionMixin):
66
71
  self.generator_text = generator
67
72
  # Attach the function dynamically
68
73
  attach_function_to_object(self, generator, "generator")
69
-
70
- self.answers = {}
71
-
72
74
 
73
- def get_body(self, **kwargs) -> ca.Section:
74
- return super().get_body()
75
+ def _build_context(self, *, rng_seed=None, **kwargs):
76
+ context = super()._build_context(rng_seed=rng_seed, **kwargs)
77
+ # Preserve prior behavior for generators that use the global random module.
78
+ random.seed(rng_seed)
79
+ return context
75
80
 
76
- def refresh(self, *args, **kwargs):
77
- super().refresh(*args, **kwargs)
81
+ def _build_body(self, context) -> Tuple[ca.Element, List[ca.Answer]]:
78
82
  try:
79
83
  generated_content = self.generator()
80
- # Expect generator to return a ca.Section or convert string to Section
81
84
  if isinstance(generated_content, ca.Section):
82
- self.text = "" # Clear text since we'll override get_body
83
- self._generated_section = generated_content
85
+ body = generated_content
84
86
  elif isinstance(generated_content, str):
85
- self.text = generated_content
86
- self._generated_section = None
87
+ body = ca.Section([ca.Text(generated_content)])
87
88
  else:
88
- # Fallback
89
- self.text = str(generated_content)
90
- self._generated_section = None
89
+ body = ca.Section([ca.Text(str(generated_content))])
91
90
  except TypeError as e:
92
91
  log.error(f"Error generating from text: {e}")
93
92
  log.debug(self.generator_text)
94
93
  exit(8)
95
94
 
96
- def get_body(self, **kwargs) -> ca.Section:
97
- if hasattr(self, '_generated_section') and self._generated_section:
98
- return self._generated_section
99
- return super().get_body()
100
-
101
-
95
+ return body, []
96
+
102
97
  class TrueFalse(FromText):
103
98
  pass