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
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
  import abc
5
5
  import difflib
6
6
  import logging
7
+ import random
7
8
 
8
9
  from QuizGenerator.question import Question, QuestionRegistry
9
10
  import QuizGenerator.contentast as ca
@@ -21,56 +22,60 @@ class IOQuestion(Question, abc.ABC):
21
22
  @QuestionRegistry.register()
22
23
  class HardDriveAccessTime(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
23
24
 
24
- def refresh(self, *args, **kwargs):
25
- super().refresh(*args, **kwargs)
26
-
27
- self.hard_drive_rotation_speed = 100 * self.rng.randint(36, 150) # e.g. 3600rpm to 15000rpm
28
- self.seek_delay = float(round(self.rng.randrange(3, 20), 2))
29
- self.transfer_rate = self.rng.randint(50, 300)
30
- self.number_of_reads = self.rng.randint(1, 20)
31
- self.size_of_reads = self.rng.randint(1, 10)
32
-
33
- self.rotational_delay = (1 / self.hard_drive_rotation_speed) * (60 / 1) * (1000 / 1) * (1/2)
34
- self.access_delay = self.rotational_delay + self.seek_delay
35
- self.transfer_delay = 1000 * (self.size_of_reads * self.number_of_reads) / 1024 / self.transfer_rate
36
- self.disk_access_delay = self.access_delay * self.number_of_reads + self.transfer_delay
37
-
38
- self.answers.update({
39
- "answer__rotational_delay" : ca.AnswerTypes.Float(self.rotational_delay),
40
- "answer__access_delay" : ca.AnswerTypes.Float(self.access_delay),
41
- "answer__transfer_delay" : ca.AnswerTypes.Float(self.transfer_delay),
42
- "answer__disk_access_delay" : ca.AnswerTypes.Float(self.disk_access_delay),
43
- })
44
-
45
- def _get_body(self, *args, **kwargs):
25
+ @classmethod
26
+ def _build_context(cls, *, rng_seed=None, **kwargs):
27
+ rng = random.Random(rng_seed)
28
+ hard_drive_rotation_speed = 100 * rng.randint(36, 150)
29
+ seek_delay = float(round(rng.randrange(3, 20), 2))
30
+ transfer_rate = rng.randint(50, 300)
31
+ number_of_reads = rng.randint(1, 20)
32
+ size_of_reads = rng.randint(1, 10)
33
+
34
+ rotational_delay = (1 / hard_drive_rotation_speed) * (60 / 1) * (1000 / 1) * (1/2)
35
+ access_delay = rotational_delay + seek_delay
36
+ transfer_delay = 1000 * (size_of_reads * number_of_reads) / 1024 / transfer_rate
37
+ disk_access_delay = access_delay * number_of_reads + transfer_delay
38
+
39
+ return {
40
+ "hard_drive_rotation_speed": hard_drive_rotation_speed,
41
+ "seek_delay": seek_delay,
42
+ "transfer_rate": transfer_rate,
43
+ "number_of_reads": number_of_reads,
44
+ "size_of_reads": size_of_reads,
45
+ "rotational_delay": rotational_delay,
46
+ "access_delay": access_delay,
47
+ "transfer_delay": transfer_delay,
48
+ "disk_access_delay": disk_access_delay,
49
+ }
50
+
51
+ @classmethod
52
+ def _build_body(cls, context):
46
53
  """Build question body and collect answers."""
47
- answers = [
48
- self.answers["answer__rotational_delay"],
49
- self.answers["answer__access_delay"],
50
- self.answers["answer__transfer_delay"],
51
- self.answers["answer__disk_access_delay"],
52
- ]
54
+ rotational_answer = ca.AnswerTypes.Float(context["rotational_delay"])
55
+ access_answer = ca.AnswerTypes.Float(context["access_delay"])
56
+ transfer_answer = ca.AnswerTypes.Float(context["transfer_delay"])
57
+ disk_access_answer = ca.AnswerTypes.Float(context["disk_access_delay"])
53
58
 
54
59
  # Create parameter info table using mixin
55
60
  parameter_info = {
56
- "Hard Drive Rotation Speed": f"{self.hard_drive_rotation_speed}RPM",
57
- "Seek Delay": f"{self.seek_delay}ms",
58
- "Transfer Rate": f"{self.transfer_rate}MB/s",
59
- "Number of Reads": f"{self.number_of_reads}",
60
- "Size of Reads": f"{self.size_of_reads}KB"
61
+ "Hard Drive Rotation Speed": f"{context['hard_drive_rotation_speed']}RPM",
62
+ "Seek Delay": f"{context['seek_delay']}ms",
63
+ "Transfer Rate": f"{context['transfer_rate']}MB/s",
64
+ "Number of Reads": f"{context['number_of_reads']}",
65
+ "Size of Reads": f"{context['size_of_reads']}KB"
61
66
  }
62
67
 
63
- parameter_table = self.create_info_table(parameter_info)
68
+ parameter_table = cls.create_info_table(parameter_info)
64
69
 
65
70
  # Create answer table with multiple rows using mixin
66
71
  answer_rows = [
67
- {"Variable": "Rotational Delay", "Value": "answer__rotational_delay"},
68
- {"Variable": "Access Delay", "Value": "answer__access_delay"},
69
- {"Variable": "Transfer Delay", "Value": "answer__transfer_delay"},
70
- {"Variable": "Total Disk Access Delay", "Value": "answer__disk_access_delay"}
72
+ {"Variable": "Rotational Delay", "Value": rotational_answer},
73
+ {"Variable": "Access Delay", "Value": access_answer},
74
+ {"Variable": "Transfer Delay", "Value": transfer_answer},
75
+ {"Variable": "Total Disk Access Delay", "Value": disk_access_answer}
71
76
  ]
72
77
 
73
- answer_table = self.create_answer_table(
78
+ answer_table = cls.create_answer_table(
74
79
  headers=["Variable", "Value"],
75
80
  data_rows=answer_rows,
76
81
  answer_columns=["Value"]
@@ -85,21 +90,17 @@ class HardDriveAccessTime(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
85
90
  f"(i.e. don't use your rounded answers to calculate your overall answer)"
86
91
  )
87
92
 
88
- body = self.create_parameter_calculation_body(
93
+ body = cls.create_parameter_calculation_body(
89
94
  intro_text=intro_text,
90
95
  parameter_table=parameter_table,
91
96
  answer_table=answer_table,
92
97
  additional_instructions=instructions
93
98
  )
94
99
 
95
- return body, answers
96
-
97
- def get_body(self, *args, **kwargs) -> ca.Section:
98
- """Build question body (backward compatible interface)."""
99
- body, _ = self._get_body(*args, **kwargs)
100
100
  return body
101
101
 
102
- def _get_explanation(self):
102
+ @classmethod
103
+ def _build_explanation(cls, context):
103
104
  explanation = ca.Section()
104
105
 
105
106
  explanation.add_element(
@@ -116,9 +117,9 @@ class HardDriveAccessTime(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
116
117
  ca.Paragraph(["Starting with the rotation delay, we calculate:"]),
117
118
  ca.Equation(
118
119
  "t_{rotation} = "
119
- + f"\\frac{{1 minute}}{{{self.hard_drive_rotation_speed}revolutions}}"
120
+ + f"\\frac{{1 minute}}{{{context['hard_drive_rotation_speed']}revolutions}}"
120
121
  + r"\cdot \frac{60 seconds}{1 minute} \cdot \frac{1000 ms}{1 second} \cdot \frac{1 revolution}{2} = "
121
- + f"{self.rotational_delay:0.2f}ms",
122
+ + f"{context['rotational_delay']:0.2f}ms",
122
123
  )
123
124
  ])
124
125
 
@@ -129,7 +130,7 @@ class HardDriveAccessTime(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
129
130
  ca.Equation(
130
131
  f"t_{{access}} "
131
132
  f"= t_{{rotation}} + t_{{seek}} "
132
- f"= {self.rotational_delay:0.2f}ms + {self.seek_delay:0.2f}ms = {self.access_delay:0.2f}ms"
133
+ f"= {context['rotational_delay']:0.2f}ms + {context['seek_delay']:0.2f}ms = {context['access_delay']:0.2f}ms"
133
134
  )
134
135
  ])
135
136
 
@@ -137,9 +138,9 @@ class HardDriveAccessTime(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
137
138
  ca.Paragraph([r"Next we need to calculate our transfer delay, $t_{transfer}$, which we do as:"]),
138
139
  ca.Equation(
139
140
  f"t_{{transfer}} "
140
- f"= \\frac{{{self.number_of_reads} \\cdot {self.size_of_reads}KB}}{{1}} \\cdot \\frac{{1MB}}{{1024KB}} "
141
- f"\\cdot \\frac{{1 second}}{{{self.transfer_rate}MB}} \\cdot \\frac{{1000ms}}{{1second}} "
142
- f"= {self.transfer_delay:0.2}ms"
141
+ f"= \\frac{{{context['number_of_reads']} \\cdot {context['size_of_reads']}KB}}{{1}} \\cdot \\frac{{1MB}}{{1024KB}} "
142
+ f"\\cdot \\frac{{1 second}}{{{context['transfer_rate']}MB}} \\cdot \\frac{{1000ms}}{{1second}} "
143
+ f"= {context['transfer_delay']:0.2}ms"
143
144
  )
144
145
  ])
145
146
 
@@ -148,70 +149,66 @@ class HardDriveAccessTime(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
148
149
  ca.Equation(
149
150
  f"t_{{total}} "
150
151
  f"= \\text{{(# reads)}} \\cdot t_{{access}} + t_{{transfer}} "
151
- f"= {self.number_of_reads} \\cdot {self.access_delay:0.2f} + {self.transfer_delay:0.2f} "
152
- f"= {self.disk_access_delay:0.2f}ms")
152
+ f"= {context['number_of_reads']} \\cdot {context['access_delay']:0.2f} + {context['transfer_delay']:0.2f} "
153
+ f"= {context['disk_access_delay']:0.2f}ms")
153
154
  ])
154
- return explanation, []
155
-
156
- def get_explanation(self) -> ca.Section:
157
- """Build question explanation (backward compatible interface)."""
158
- explanation, _ = self._get_explanation()
159
155
  return explanation
160
156
 
161
157
 
162
158
  @QuestionRegistry.register()
163
159
  class INodeAccesses(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
164
160
 
165
- def refresh(self, *args, **kwargs):
166
- super().refresh(*args, **kwargs)
167
-
168
- # Calculating this first to use blocksize as an even multiple of it
169
- self.inode_size = 2**self.rng.randint(6, 10)
170
-
171
- self.block_size = self.inode_size * self.rng.randint(8, 20)
172
- self.inode_number = self.rng.randint(0, 256)
173
- self.inode_start_location = self.block_size * self.rng.randint(2, 5)
174
-
175
- self.inode_address = self.inode_start_location + self.inode_number * self.inode_size
176
- self.inode_block = self.inode_address // self.block_size
177
- self.inode_address_in_block = self.inode_address % self.block_size
178
- self.inode_index_in_block = int(self.inode_address_in_block / self.inode_size)
179
-
180
- self.answers.update({
181
- "answer__inode_address": ca.AnswerTypes.Int(self.inode_address),
182
- "answer__inode_block": ca.AnswerTypes.Int(self.inode_block),
183
- "answer__inode_address_in_block": ca.AnswerTypes.Int(self.inode_address_in_block),
184
- "answer__inode_index_in_block": ca.AnswerTypes.Int(self.inode_index_in_block),
185
- })
186
-
187
- def _get_body(self):
161
+ @classmethod
162
+ def _build_context(cls, *, rng_seed=None, **kwargs):
163
+ rng = random.Random(rng_seed)
164
+ inode_size = 2**rng.randint(6, 10)
165
+ block_size = inode_size * rng.randint(8, 20)
166
+ inode_number = rng.randint(0, 256)
167
+ inode_start_location = block_size * rng.randint(2, 5)
168
+
169
+ inode_address = inode_start_location + inode_number * inode_size
170
+ inode_block = inode_address // block_size
171
+ inode_address_in_block = inode_address % block_size
172
+ inode_index_in_block = int(inode_address_in_block / inode_size)
173
+
174
+ return {
175
+ "inode_size": inode_size,
176
+ "block_size": block_size,
177
+ "inode_number": inode_number,
178
+ "inode_start_location": inode_start_location,
179
+ "inode_address": inode_address,
180
+ "inode_block": inode_block,
181
+ "inode_address_in_block": inode_address_in_block,
182
+ "inode_index_in_block": inode_index_in_block,
183
+ }
184
+
185
+ @classmethod
186
+ def _build_body(cls, context):
188
187
  """Build question body and collect answers."""
189
- answers = [
190
- self.answers["answer__inode_address"],
191
- self.answers["answer__inode_block"],
192
- self.answers["answer__inode_address_in_block"],
193
- self.answers["answer__inode_index_in_block"],
194
- ]
188
+ inode_address_answer = ca.AnswerTypes.Int(context["inode_address"])
189
+ inode_block_answer = ca.AnswerTypes.Int(context["inode_block"])
190
+ inode_address_in_block_answer = ca.AnswerTypes.Int(context["inode_address_in_block"])
191
+ inode_index_in_block_answer = ca.AnswerTypes.Int(context["inode_index_in_block"])
195
192
 
196
193
  # Create parameter info table using mixin
197
194
  parameter_info = {
198
- "Block Size": f"{self.block_size} Bytes",
199
- "Inode Number": f"{self.inode_number}",
200
- "Inode Start Location": f"{self.inode_start_location} Bytes",
201
- "Inode size": f"{self.inode_size} Bytes"
195
+ "Block Size": f"{context['block_size']} Bytes",
196
+ "Inode Number": f"{context['inode_number']}",
197
+ "Inode Start Location": f"{context['inode_start_location']} Bytes",
198
+ "Inode size": f"{context['inode_size']} Bytes"
202
199
  }
203
200
 
204
- parameter_table = self.create_info_table(parameter_info)
201
+ parameter_table = cls.create_info_table(parameter_info)
205
202
 
206
203
  # Create answer table with multiple rows using mixin
207
204
  answer_rows = [
208
- {"Variable": "Inode address", "Value": "answer__inode_address"},
209
- {"Variable": "Block containing inode", "Value": "answer__inode_block"},
210
- {"Variable": "Inode address (offset) within block", "Value": "answer__inode_address_in_block"},
211
- {"Variable": "Inode index within block", "Value": "answer__inode_index_in_block"}
205
+ {"Variable": "Inode address", "Value": inode_address_answer},
206
+ {"Variable": "Block containing inode", "Value": inode_block_answer},
207
+ {"Variable": "Inode address (offset) within block", "Value": inode_address_in_block_answer},
208
+ {"Variable": "Inode index within block", "Value": inode_index_in_block_answer}
212
209
  ]
213
210
 
214
- answer_table = self.create_answer_table(
211
+ answer_table = cls.create_answer_table(
215
212
  headers=["Variable", "Value"],
216
213
  data_rows=answer_rows,
217
214
  answer_columns=["Value"]
@@ -220,21 +217,17 @@ class INodeAccesses(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
220
217
  # Use mixin to create complete body with both tables
221
218
  intro_text = "Given the information below, please calculate the following values."
222
219
 
223
- body = self.create_parameter_calculation_body(
220
+ body = cls.create_parameter_calculation_body(
224
221
  intro_text=intro_text,
225
222
  parameter_table=parameter_table,
226
223
  answer_table=answer_table,
227
224
  # additional_instructions=instructions
228
225
  )
229
226
 
230
- return body, answers
231
-
232
- def get_body(self) -> ca.Section:
233
- """Build question body (backward compatible interface)."""
234
- body, _ = self._get_body()
235
227
  return body
236
228
 
237
- def _get_explanation(self):
229
+ @classmethod
230
+ def _build_explanation(cls, context):
238
231
  explanation = ca.Section()
239
232
 
240
233
  explanation.add_element(
@@ -251,8 +244,8 @@ class INodeAccesses(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
251
244
  r"(\text{Inode address})",
252
245
  [
253
246
  r"(\text{Inode Start Location}) + (\text{inode #}) \cdot (\text{inode size})",
254
- f"{self.inode_start_location} + {self.inode_number} \\cdot {self.inode_size}",
255
- f"{self.inode_address}"
247
+ f"{context['inode_start_location']} + {context['inode_number']} \\cdot {context['inode_size']}",
248
+ f"{context['inode_address']}"
256
249
  ])
257
250
  )
258
251
 
@@ -267,8 +260,8 @@ class INodeAccesses(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
267
260
  r"\text{Block containing inode}",
268
261
  [
269
262
  r"(\text{Inode address}) \mathbin{//} (\text{block size})",
270
- f"{self.inode_address} \\mathbin{{//}} {self.block_size}",
271
- f"{self.inode_block}"
263
+ f"{context['inode_address']} \\mathbin{{//}} {context['block_size']}",
264
+ f"{context['inode_block']}"
272
265
  ]
273
266
  ))
274
267
 
@@ -287,8 +280,8 @@ class INodeAccesses(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
287
280
  r"\text{offset within block}",
288
281
  [
289
282
  r"(\text{Inode address}) \bmod (\text{block size})",
290
- f"{self.inode_address} \\bmod {self.block_size}",
291
- f"{self.inode_address_in_block}"
283
+ f"{context['inode_address']} \\bmod {context['block_size']}",
284
+ f"{context['inode_address_in_block']}"
292
285
  ]
293
286
  ))
294
287
 
@@ -302,16 +295,11 @@ class INodeAccesses(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
302
295
  r"\text{index within block}",
303
296
  [
304
297
  r"\dfrac{\text{offset within block}}{\text{inode size}}",
305
- f"\\dfrac{{{self.inode_address_in_block}}}{{{self.inode_size}}}",
306
- f"{self.inode_index_in_block}"
298
+ f"\\dfrac{{{context['inode_address_in_block']}}}{{{context['inode_size']}}}",
299
+ f"{context['inode_index_in_block']}"
307
300
  ]
308
301
  ))
309
302
 
310
- return explanation, []
311
-
312
- def get_explanation(self) -> ca.Section:
313
- """Build question explanation (backward compatible interface)."""
314
- explanation, _ = self._get_explanation()
315
303
  return explanation
316
304
 
317
305
 
@@ -323,69 +311,61 @@ class VSFS_states(IOQuestion):
323
311
  def __init__(self, *args, **kwargs):
324
312
  super().__init__(*args, **kwargs)
325
313
  self.answer_kind = ca.Answer.CanvasAnswerKind.MULTIPLE_DROPDOWN
326
-
327
- self.num_steps = kwargs.get("num_steps", 10)
328
-
329
- def refresh(self, *args, **kwargs):
330
- super().refresh(*args, **kwargs)
331
-
332
- fs = self.vsfs(4, 4, self.rng)
333
- operations = fs.run_for_steps(self.num_steps)
334
-
335
- self.start_state = operations[-1]["start_state"]
336
- self.end_state = operations[-1]["end_state"]
337
-
338
- wrong_answers = list(filter(
339
- lambda o: o != operations[-1]["cmd"],
340
- map(
341
- lambda o: o["cmd"],
342
- operations
343
- )
344
- ))
345
- self.rng.shuffle(wrong_answers)
346
-
347
- self.answers["answer__cmd"] = ca.Answer.dropdown(
314
+
315
+ @classmethod
316
+ def _build_context(cls, *, rng_seed=None, **kwargs):
317
+ rng = random.Random(rng_seed)
318
+ num_steps = kwargs.get("num_steps", 10)
319
+
320
+ fs = cls.vsfs(4, 4, rng)
321
+ operations = fs.run_for_steps(num_steps)
322
+
323
+ start_state = operations[-1]["start_state"]
324
+ end_state = operations[-1]["end_state"]
325
+
326
+ command_answer = ca.Answer.dropdown(
348
327
  f"{operations[-1]['cmd']}",
349
328
  baffles=list(set([op['cmd'] for op in operations[:-1] if op != operations[-1]['cmd']])),
350
329
  label="Command"
351
330
  )
352
-
353
- def _get_body(self):
354
- """Build question body and collect answers."""
355
- answers = [self.answers["answer__cmd"]]
356
331
 
332
+ return {
333
+ "start_state": start_state,
334
+ "end_state": end_state,
335
+ "command_answer": command_answer,
336
+ }
337
+
338
+ @classmethod
339
+ def _build_body(cls, context):
340
+ """Build question body and collect answers."""
357
341
  body = ca.Section()
358
342
 
359
343
  body.add_element(ca.Paragraph(["What operation happens between these two states?"]))
360
344
 
361
345
  body.add_element(
362
346
  ca.Code(
363
- self.start_state,
347
+ context["start_state"],
364
348
  make_small=True
365
349
  )
366
350
  )
367
351
 
368
- body.add_element(ca.AnswerBlock(self.answers["answer__cmd"]))
352
+ body.add_element(ca.AnswerBlock(context["command_answer"]))
369
353
 
370
354
  body.add_element(
371
355
  ca.Code(
372
- self.end_state,
356
+ context["end_state"],
373
357
  make_small=True
374
358
  )
375
359
  )
376
360
 
377
- return body, answers
378
-
379
- def get_body(self) -> ca.Section:
380
- """Build question body (backward compatible interface)."""
381
- body, _ = self._get_body()
382
361
  return body
383
362
 
384
- def _get_explanation(self):
363
+ @classmethod
364
+ def _build_explanation(cls, context):
385
365
  explanation = ca.Section()
386
366
 
387
- log.debug(f"self.start_state: {self.start_state}")
388
- log.debug(f"self.end_state: {self.end_state}")
367
+ log.debug(f"start_state: {context['start_state']}")
368
+ log.debug(f"end_state: {context['end_state']}")
389
369
 
390
370
  explanation.add_elements([
391
371
  ca.Paragraph([
@@ -396,7 +376,7 @@ class VSFS_states(IOQuestion):
396
376
 
397
377
  chunk_to_add = []
398
378
  lines_that_changed = []
399
- for start_line, end_line in zip(self.start_state.split('\n'), self.end_state.split('\n')):
379
+ for start_line, end_line in zip(context["start_state"].split('\n'), context["end_state"].split('\n')):
400
380
  if start_line == end_line:
401
381
  continue
402
382
  lines_that_changed.append((start_line, end_line))
@@ -461,14 +441,8 @@ class VSFS_states(IOQuestion):
461
441
 
462
442
  explanation.add_element(
463
443
  ca.Code(
464
- highlight_changes(self.start_state, self.end_state)
444
+ highlight_changes(context["start_state"], context["end_state"])
465
445
  )
466
446
  )
467
447
 
468
- return explanation, []
469
-
470
- def get_explanation(self) -> ca.Section:
471
- """Build question explanation (backward compatible interface)."""
472
- explanation, _ = self._get_explanation()
473
448
  return explanation
474
-