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.
- QuizGenerator/contentast.py +6 -6
- QuizGenerator/generate.py +2 -1
- QuizGenerator/mixins.py +14 -100
- QuizGenerator/premade_questions/basic.py +24 -29
- QuizGenerator/premade_questions/cst334/languages.py +100 -99
- QuizGenerator/premade_questions/cst334/math_questions.py +112 -122
- QuizGenerator/premade_questions/cst334/memory_questions.py +621 -621
- QuizGenerator/premade_questions/cst334/persistence_questions.py +137 -163
- QuizGenerator/premade_questions/cst334/process.py +312 -322
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +34 -35
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +41 -36
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +48 -41
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +285 -520
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +149 -126
- QuizGenerator/premade_questions/cst463/models/attention.py +44 -50
- QuizGenerator/premade_questions/cst463/models/cnns.py +43 -47
- QuizGenerator/premade_questions/cst463/models/matrices.py +61 -11
- QuizGenerator/premade_questions/cst463/models/rnns.py +48 -50
- QuizGenerator/premade_questions/cst463/models/text.py +65 -67
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +47 -46
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +100 -156
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +93 -141
- QuizGenerator/question.py +273 -202
- QuizGenerator/quiz.py +8 -5
- QuizGenerator/regenerate.py +128 -19
- {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/METADATA +30 -2
- {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/RECORD +30 -30
- {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/WHEEL +0 -0
- {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/contentast.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
662
|
-
- Question explanation/solution content (return from
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
29
|
-
return ca.
|
|
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
|
-
|
|
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
|
|
74
|
-
|
|
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
|
|
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
|
-
|
|
83
|
-
self._generated_section = generated_content
|
|
85
|
+
body = generated_content
|
|
84
86
|
elif isinstance(generated_content, str):
|
|
85
|
-
|
|
86
|
-
self._generated_section = None
|
|
87
|
+
body = ca.Section([ca.Text(generated_content)])
|
|
87
88
|
else:
|
|
88
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
return self._generated_section
|
|
99
|
-
return super().get_body()
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
return body, []
|
|
96
|
+
|
|
102
97
|
class TrueFalse(FromText):
|
|
103
98
|
pass
|