QuizGenerator 0.8.1__py3-none-any.whl → 0.10.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/README.md +5 -0
- QuizGenerator/canvas/canvas_interface.py +6 -2
- QuizGenerator/contentast.py +33 -11
- QuizGenerator/generate.py +51 -10
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/mixins.py +6 -2
- QuizGenerator/premade_questions/basic.py +49 -7
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +92 -82
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +68 -45
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +238 -162
- QuizGenerator/premade_questions/cst463/models/attention.py +0 -1
- QuizGenerator/premade_questions/cst463/models/cnns.py +0 -1
- QuizGenerator/premade_questions/cst463/models/rnns.py +0 -1
- QuizGenerator/premade_questions/cst463/models/text.py +0 -1
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +20 -1
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +51 -45
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +212 -215
- QuizGenerator/qrcode_generator.py +116 -54
- QuizGenerator/question.py +168 -23
- QuizGenerator/regenerate.py +23 -9
- {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/METADATA +34 -22
- {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/RECORD +25 -23
- {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/WHEEL +0 -0
- {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/README.md
ADDED
|
@@ -37,8 +37,12 @@ NUM_WORKERS = 4
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
class CanvasInterface:
|
|
40
|
-
def __init__(self, *, prod=False):
|
|
41
|
-
|
|
40
|
+
def __init__(self, *, prod=False, env_path: str | None = None):
|
|
41
|
+
default_env = os.path.join(os.path.expanduser("~"), ".env")
|
|
42
|
+
if env_path and os.path.exists(env_path):
|
|
43
|
+
dotenv.load_dotenv(env_path)
|
|
44
|
+
elif os.path.exists(default_env):
|
|
45
|
+
dotenv.load_dotenv(default_env)
|
|
42
46
|
|
|
43
47
|
self.prod = prod
|
|
44
48
|
if self.prod:
|
QuizGenerator/contentast.py
CHANGED
|
@@ -23,6 +23,21 @@ import numpy as np
|
|
|
23
23
|
|
|
24
24
|
log = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
|
+
_PANDOC_OK = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _ensure_pandoc() -> bool:
|
|
30
|
+
global _PANDOC_OK
|
|
31
|
+
if _PANDOC_OK is not None:
|
|
32
|
+
return _PANDOC_OK
|
|
33
|
+
try:
|
|
34
|
+
pypandoc.get_pandoc_version()
|
|
35
|
+
_PANDOC_OK = True
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
_PANDOC_OK = False
|
|
38
|
+
log.warning(f"Pandoc not found or unusable: {exc}")
|
|
39
|
+
return _PANDOC_OK
|
|
40
|
+
|
|
26
41
|
|
|
27
42
|
"""
|
|
28
43
|
Content Abstract Syntax Tree - The core content system for quiz generation.
|
|
@@ -221,6 +236,9 @@ class Leaf(Element):
|
|
|
221
236
|
return html_output.strip()
|
|
222
237
|
|
|
223
238
|
case _:
|
|
239
|
+
if not _ensure_pandoc():
|
|
240
|
+
log.warning(f"Pandoc unavailable; returning raw markdown for {output_format} output.")
|
|
241
|
+
return str_to_convert
|
|
224
242
|
output = pypandoc.convert_text(
|
|
225
243
|
str_to_convert,
|
|
226
244
|
output_format,
|
|
@@ -549,16 +567,18 @@ class Question(Container):
|
|
|
549
567
|
|
|
550
568
|
# Build extra_data dict with regeneration metadata if available
|
|
551
569
|
extra_data = {}
|
|
552
|
-
if hasattr(self, 'question_class_name') and hasattr(self, 'generation_seed')
|
|
553
|
-
|
|
554
|
-
):
|
|
555
|
-
if self.question_class_name and self.generation_seed is not None and self.question_version:
|
|
570
|
+
if hasattr(self, 'question_class_name') and hasattr(self, 'generation_seed'):
|
|
571
|
+
if self.question_class_name and self.generation_seed is not None:
|
|
556
572
|
extra_data['question_type'] = self.question_class_name
|
|
557
573
|
extra_data['seed'] = self.generation_seed
|
|
558
|
-
|
|
574
|
+
if hasattr(self, 'question_version') and self.question_version:
|
|
575
|
+
extra_data['version'] = self.question_version
|
|
559
576
|
# Include question-specific configuration parameters if available
|
|
560
577
|
if hasattr(self, 'config_params') and self.config_params:
|
|
561
578
|
extra_data['config'] = self.config_params
|
|
579
|
+
# Include context-derived extras if available
|
|
580
|
+
if hasattr(self, 'qr_context_extras') and self.qr_context_extras:
|
|
581
|
+
extra_data['context'] = self.qr_context_extras
|
|
562
582
|
|
|
563
583
|
qr_path = QuestionQRCode.generate_qr_pdf(
|
|
564
584
|
self.question_number,
|
|
@@ -615,16 +635,18 @@ class Question(Container):
|
|
|
615
635
|
|
|
616
636
|
# Build extra_data dict with regeneration metadata if available
|
|
617
637
|
extra_data = {}
|
|
618
|
-
if hasattr(self, 'question_class_name') and hasattr(self, 'generation_seed')
|
|
619
|
-
|
|
620
|
-
):
|
|
621
|
-
if self.question_class_name and self.generation_seed is not None and self.question_version:
|
|
638
|
+
if hasattr(self, 'question_class_name') and hasattr(self, 'generation_seed'):
|
|
639
|
+
if self.question_class_name and self.generation_seed is not None:
|
|
622
640
|
extra_data['question_type'] = self.question_class_name
|
|
623
641
|
extra_data['seed'] = self.generation_seed
|
|
624
|
-
|
|
642
|
+
if hasattr(self, 'question_version') and self.question_version:
|
|
643
|
+
extra_data['version'] = self.question_version
|
|
625
644
|
# Include question-specific configuration parameters if available
|
|
626
645
|
if hasattr(self, 'config_params') and self.config_params:
|
|
627
646
|
extra_data['config'] = self.config_params
|
|
647
|
+
# Include context-derived extras if available
|
|
648
|
+
if hasattr(self, 'qr_context_extras') and self.qr_context_extras:
|
|
649
|
+
extra_data['context'] = self.qr_context_extras
|
|
628
650
|
|
|
629
651
|
# Generate QR code PNG
|
|
630
652
|
qr_path = QuestionQRCode.generate_qr_pdf(
|
|
@@ -668,7 +690,7 @@ class Section(Container):
|
|
|
668
690
|
- Organizing complex question content
|
|
669
691
|
|
|
670
692
|
Example:
|
|
671
|
-
def _build_body(
|
|
693
|
+
def _build_body(cls, context):
|
|
672
694
|
body = Section()
|
|
673
695
|
answers = []
|
|
674
696
|
body.add_element(Paragraph(["Calculate the determinant:"]))
|
QuizGenerator/generate.py
CHANGED
|
@@ -32,7 +32,18 @@ def parse_args():
|
|
|
32
32
|
|
|
33
33
|
parser.add_argument("--debug", action="store_true", help="Set logging level to debug")
|
|
34
34
|
|
|
35
|
-
parser.add_argument(
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--yaml",
|
|
37
|
+
dest="quiz_yaml",
|
|
38
|
+
default=None,
|
|
39
|
+
help="Path to quiz YAML configuration"
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--quiz_yaml",
|
|
43
|
+
dest="quiz_yaml",
|
|
44
|
+
default=None,
|
|
45
|
+
help=argparse.SUPPRESS # Backwards-compatible alias for --yaml
|
|
46
|
+
)
|
|
36
47
|
parser.add_argument("--seed", type=int, default=None,
|
|
37
48
|
help="Random seed for quiz generation (default: None for random)")
|
|
38
49
|
|
|
@@ -45,7 +56,10 @@ def parse_args():
|
|
|
45
56
|
|
|
46
57
|
# PDF Flags
|
|
47
58
|
parser.add_argument("--num_pdfs", default=0, type=int, help="How many PDF quizzes to create")
|
|
48
|
-
parser.add_argument("--latex", action="store_false", dest="typst", help="Use
|
|
59
|
+
parser.add_argument("--latex", action="store_false", dest="typst", help="Use LaTeX instead of Typst for PDF generation")
|
|
60
|
+
parser.set_defaults(typst=True)
|
|
61
|
+
parser.add_argument("--typst_measurement", action="store_true",
|
|
62
|
+
help="Use Typst measurement for layout optimization (experimental)")
|
|
49
63
|
|
|
50
64
|
# Testing flags
|
|
51
65
|
parser.add_argument("--test_all", type=int, default=0, metavar="N",
|
|
@@ -54,6 +68,8 @@ def parse_args():
|
|
|
54
68
|
help="Only test specific question types by name (use with --test_all)")
|
|
55
69
|
parser.add_argument("--strict", action="store_true",
|
|
56
70
|
help="With --test_all, skip PDF/Canvas generation if any questions fail")
|
|
71
|
+
parser.add_argument("--skip_missing_extras", action="store_true",
|
|
72
|
+
help="With --test_all, skip questions that fail due to missing optional dependencies")
|
|
57
73
|
|
|
58
74
|
subparsers = parser.add_subparsers(dest='command')
|
|
59
75
|
test_parser = subparsers.add_parser("TEST")
|
|
@@ -65,6 +81,10 @@ def parse_args():
|
|
|
65
81
|
log.error("Must provide course_id when pushing to canvas")
|
|
66
82
|
exit(8)
|
|
67
83
|
|
|
84
|
+
if args.test_all <= 0 and not args.quiz_yaml:
|
|
85
|
+
log.error("Must provide --yaml unless using --test_all")
|
|
86
|
+
exit(8)
|
|
87
|
+
|
|
68
88
|
return args
|
|
69
89
|
|
|
70
90
|
|
|
@@ -82,7 +102,8 @@ def test_all_questions(
|
|
|
82
102
|
use_typst: bool = True,
|
|
83
103
|
canvas_course=None,
|
|
84
104
|
strict: bool = False,
|
|
85
|
-
question_filter: list = None
|
|
105
|
+
question_filter: list = None,
|
|
106
|
+
skip_missing_extras: bool = False
|
|
86
107
|
):
|
|
87
108
|
"""
|
|
88
109
|
Test all registered questions by generating N variations of each.
|
|
@@ -129,6 +150,7 @@ def test_all_questions(
|
|
|
129
150
|
print("=" * 70)
|
|
130
151
|
|
|
131
152
|
failed_questions = []
|
|
153
|
+
skipped_questions = []
|
|
132
154
|
successful_questions = []
|
|
133
155
|
# Collect question instances for PDF/Canvas generation
|
|
134
156
|
test_question_instances = []
|
|
@@ -153,7 +175,7 @@ def test_all_questions(
|
|
|
153
175
|
)
|
|
154
176
|
|
|
155
177
|
# Generate the question (this calls refresh and builds the AST)
|
|
156
|
-
instance = question.instantiate(rng_seed=seed)
|
|
178
|
+
instance = question.instantiate(rng_seed=seed, max_backoff_attempts=200)
|
|
157
179
|
question_ast = question._build_question_ast(instance)
|
|
158
180
|
|
|
159
181
|
# Try rendering to both formats to catch format-specific issues
|
|
@@ -174,6 +196,14 @@ def test_all_questions(
|
|
|
174
196
|
# If we got here, the question works - save the instance
|
|
175
197
|
test_question_instances.append(question)
|
|
176
198
|
|
|
199
|
+
except ImportError as e:
|
|
200
|
+
if skip_missing_extras:
|
|
201
|
+
skipped_questions.append(question_name)
|
|
202
|
+
log.warning(f"Skipping {question_name} due to missing optional dependency: {e}")
|
|
203
|
+
question_failures = []
|
|
204
|
+
break
|
|
205
|
+
tb = traceback.format_exc()
|
|
206
|
+
question_failures.append(f" Variation {variation+1}: Generation failed - {e}\n{tb}")
|
|
177
207
|
except Exception as e:
|
|
178
208
|
tb = traceback.format_exc()
|
|
179
209
|
question_failures.append(f" Variation {variation+1}: Generation failed - {e}\n{tb}")
|
|
@@ -183,6 +213,8 @@ def test_all_questions(
|
|
|
183
213
|
for failure in question_failures:
|
|
184
214
|
print(failure)
|
|
185
215
|
failed_questions.append((question_name, question_failures))
|
|
216
|
+
elif question_name in skipped_questions:
|
|
217
|
+
print(" SKIPPED (missing optional dependency)")
|
|
186
218
|
else:
|
|
187
219
|
print(f" OK ({num_variations}/{num_variations} variations)")
|
|
188
220
|
successful_questions.append(question_name)
|
|
@@ -194,11 +226,17 @@ def test_all_questions(
|
|
|
194
226
|
print(f"Total question types: {total_questions}")
|
|
195
227
|
print(f"Successful: {len(successful_questions)}")
|
|
196
228
|
print(f"Failed: {len(failed_questions)}")
|
|
229
|
+
if skipped_questions:
|
|
230
|
+
print(f"Skipped (missing extras): {len(set(skipped_questions))}")
|
|
197
231
|
|
|
198
232
|
if failed_questions:
|
|
199
233
|
print("\nFailed questions:")
|
|
200
234
|
for name, failures in failed_questions:
|
|
201
235
|
print(f" - {name}: {len(failures)} failures")
|
|
236
|
+
if skipped_questions:
|
|
237
|
+
print("\nSkipped questions (missing extras):")
|
|
238
|
+
for name in sorted(set(skipped_questions)):
|
|
239
|
+
print(f" - {name}")
|
|
202
240
|
|
|
203
241
|
print("=" * 70)
|
|
204
242
|
|
|
@@ -367,14 +405,15 @@ def generate_quiz(
|
|
|
367
405
|
delete_assignment_group=False,
|
|
368
406
|
use_typst=False,
|
|
369
407
|
use_typst_measurement=False,
|
|
370
|
-
base_seed=None
|
|
408
|
+
base_seed=None,
|
|
409
|
+
env_path=None
|
|
371
410
|
):
|
|
372
411
|
|
|
373
412
|
quizzes = Quiz.from_yaml(path_to_quiz_yaml)
|
|
374
413
|
|
|
375
414
|
# Handle Canvas uploads with shared assignment group
|
|
376
415
|
if num_canvas > 0:
|
|
377
|
-
canvas_interface = CanvasInterface(prod=use_prod)
|
|
416
|
+
canvas_interface = CanvasInterface(prod=use_prod, env_path=env_path)
|
|
378
417
|
canvas_course = canvas_interface.get_course(course_id=course_id)
|
|
379
418
|
|
|
380
419
|
# Create assignment group once, with delete flag if specified
|
|
@@ -448,7 +487,7 @@ def main():
|
|
|
448
487
|
# Set up Canvas course if course_id provided
|
|
449
488
|
canvas_course = None
|
|
450
489
|
if args.course_id:
|
|
451
|
-
canvas_interface = CanvasInterface(prod=args.prod)
|
|
490
|
+
canvas_interface = CanvasInterface(prod=args.prod, env_path=args.env)
|
|
452
491
|
canvas_course = canvas_interface.get_course(course_id=args.course_id)
|
|
453
492
|
|
|
454
493
|
success = test_all_questions(
|
|
@@ -457,7 +496,8 @@ def main():
|
|
|
457
496
|
use_typst=getattr(args, 'typst', True),
|
|
458
497
|
canvas_course=canvas_course,
|
|
459
498
|
strict=args.strict,
|
|
460
|
-
question_filter=args.test_questions
|
|
499
|
+
question_filter=args.test_questions,
|
|
500
|
+
skip_missing_extras=args.skip_missing_extras
|
|
461
501
|
)
|
|
462
502
|
exit(0 if success else 1)
|
|
463
503
|
|
|
@@ -471,9 +511,10 @@ def main():
|
|
|
471
511
|
use_prod=args.prod,
|
|
472
512
|
course_id=args.course_id,
|
|
473
513
|
delete_assignment_group=getattr(args, 'delete_assignment_group', False),
|
|
474
|
-
use_typst=getattr(args, 'typst',
|
|
514
|
+
use_typst=getattr(args, 'typst', True),
|
|
475
515
|
use_typst_measurement=getattr(args, 'typst_measurement', False),
|
|
476
|
-
base_seed=getattr(args, 'seed', None)
|
|
516
|
+
base_seed=getattr(args, 'seed', None),
|
|
517
|
+
env_path=args.env
|
|
477
518
|
)
|
|
478
519
|
|
|
479
520
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
disable_existing_loggers: false
|
|
3
|
+
|
|
4
|
+
formatters:
|
|
5
|
+
standard:
|
|
6
|
+
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
7
|
+
datefmt: '%Y-%m-%d %H:%M:%S'
|
|
8
|
+
|
|
9
|
+
detailed:
|
|
10
|
+
format: '%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(funcName)s:%(lineno)d - %(message)s'
|
|
11
|
+
datefmt: '%Y-%m-%d %H:%M:%S'
|
|
12
|
+
|
|
13
|
+
simple:
|
|
14
|
+
format: '%(levelname)s - %(message)s'
|
|
15
|
+
|
|
16
|
+
handlers:
|
|
17
|
+
console:
|
|
18
|
+
class: logging.StreamHandler
|
|
19
|
+
level: INFO
|
|
20
|
+
formatter: standard
|
|
21
|
+
stream: ext://sys.stdout
|
|
22
|
+
|
|
23
|
+
file:
|
|
24
|
+
class: logging.FileHandler
|
|
25
|
+
level: INFO
|
|
26
|
+
formatter: detailed
|
|
27
|
+
filename: ${LOG_FILE:-teachingtools.log}
|
|
28
|
+
mode: a
|
|
29
|
+
|
|
30
|
+
error_file:
|
|
31
|
+
class: logging.FileHandler
|
|
32
|
+
level: ERROR
|
|
33
|
+
formatter: detailed
|
|
34
|
+
filename: ${ERROR_LOG_FILE:-teachingtools_errors.log}
|
|
35
|
+
mode: a
|
|
36
|
+
|
|
37
|
+
loggers:
|
|
38
|
+
QuizGenerator:
|
|
39
|
+
level: INFO
|
|
40
|
+
handlers: [console, file]
|
|
41
|
+
propagate: false
|
|
42
|
+
|
|
43
|
+
lms_interface:
|
|
44
|
+
level: INFO
|
|
45
|
+
handlers: [console, file]
|
|
46
|
+
propagate: false
|
|
47
|
+
|
|
48
|
+
canvasapi:
|
|
49
|
+
level: WARNING
|
|
50
|
+
handlers: [console]
|
|
51
|
+
propagate: false
|
|
52
|
+
|
|
53
|
+
root:
|
|
54
|
+
level: INFO
|
|
55
|
+
handlers: [console, file, error_file]
|
QuizGenerator/mixins.py
CHANGED
|
@@ -419,8 +419,10 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
|
|
|
419
419
|
subparts.append((operand_a_latex, self.get_operator(), operand_b_latex))
|
|
420
420
|
return subparts
|
|
421
421
|
|
|
422
|
-
|
|
422
|
+
@classmethod
|
|
423
|
+
def _build_body(cls, context):
|
|
423
424
|
"""Build question body and collect answers."""
|
|
425
|
+
self = context
|
|
424
426
|
body = ca.Section()
|
|
425
427
|
answers = []
|
|
426
428
|
|
|
@@ -453,8 +455,10 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
|
|
|
453
455
|
# Default implementation - subclasses should override for specific answer formats
|
|
454
456
|
return []
|
|
455
457
|
|
|
456
|
-
|
|
458
|
+
@classmethod
|
|
459
|
+
def _build_explanation(cls, context):
|
|
457
460
|
"""Default explanation structure. Subclasses should override for specific explanations."""
|
|
461
|
+
self = context
|
|
458
462
|
explanation = ca.Section()
|
|
459
463
|
|
|
460
464
|
explanation.add_element(ca.Paragraph([self.get_explanation_intro()]))
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from typing import Tuple, List
|
|
5
|
+
from types import SimpleNamespace
|
|
5
6
|
import random
|
|
6
7
|
|
|
7
8
|
import logging
|
|
@@ -17,19 +18,23 @@ log = logging.getLogger(__name__)
|
|
|
17
18
|
class FromText(Question):
|
|
18
19
|
|
|
19
20
|
def __init__(self, *args, text, **kwargs):
|
|
21
|
+
kwargs["text"] = text
|
|
20
22
|
super().__init__(*args, **kwargs)
|
|
21
23
|
self.text = text
|
|
22
24
|
self.possible_variations = 1
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
@classmethod
|
|
27
|
+
def _build_context(cls, *, rng_seed=None, **kwargs):
|
|
25
28
|
context = super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
26
|
-
context["text"] =
|
|
29
|
+
context["text"] = kwargs.get("text", "")
|
|
27
30
|
return context
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
@classmethod
|
|
33
|
+
def _build_body(cls, context) -> Tuple[ca.Element, List[ca.Answer]]:
|
|
30
34
|
return ca.Section([ca.Text(context["text"])]), []
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
@classmethod
|
|
37
|
+
def _build_explanation(cls, context) -> Tuple[ca.Element, List[ca.Answer]]:
|
|
33
38
|
return ca.Section(), []
|
|
34
39
|
|
|
35
40
|
|
|
@@ -43,6 +48,7 @@ class FromGenerator(FromText, TableQuestionMixin):
|
|
|
43
48
|
if generator is None:
|
|
44
49
|
generator = text
|
|
45
50
|
|
|
51
|
+
kwargs["generator"] = generator
|
|
46
52
|
super().__init__(*args, text="", **kwargs)
|
|
47
53
|
self.possible_variations = kwargs.get("possible_variations", float('inf'))
|
|
48
54
|
|
|
@@ -72,15 +78,51 @@ class FromGenerator(FromText, TableQuestionMixin):
|
|
|
72
78
|
# Attach the function dynamically
|
|
73
79
|
attach_function_to_object(self, generator, "generator")
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _compile_generator(function_code, function_name="generator"):
|
|
83
|
+
# Provide a deterministic RNG handle for generator snippets.
|
|
84
|
+
function_code = "rng = self.rng\n" + function_code
|
|
85
|
+
|
|
86
|
+
local_namespace = {
|
|
87
|
+
'ca': ca,
|
|
88
|
+
'Section': ca.Section,
|
|
89
|
+
'Text': ca.Text,
|
|
90
|
+
'Table': ca.Table,
|
|
91
|
+
'Paragraph': ca.Paragraph
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
exec_globals = {**globals(), **local_namespace}
|
|
95
|
+
exec(
|
|
96
|
+
f"def {function_name}(self):\n" + "\n".join(f" {line}" for line in function_code.splitlines()),
|
|
97
|
+
exec_globals,
|
|
98
|
+
local_namespace
|
|
99
|
+
)
|
|
100
|
+
return local_namespace[function_name]
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def _build_context(cls, *, rng_seed=None, **kwargs):
|
|
76
104
|
context = super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
105
|
+
for key, value in kwargs.items():
|
|
106
|
+
if key not in context:
|
|
107
|
+
context[key] = value
|
|
77
108
|
# Preserve prior behavior for generators that use the global random module.
|
|
78
109
|
random.seed(rng_seed)
|
|
110
|
+
generator_text = kwargs.get("generator")
|
|
111
|
+
if generator_text is not None:
|
|
112
|
+
context["generator_fn"] = cls._compile_generator(generator_text)
|
|
113
|
+
context["generator_scope"] = SimpleNamespace(
|
|
114
|
+
rng=context.rng,
|
|
115
|
+
**context.data
|
|
116
|
+
)
|
|
79
117
|
return context
|
|
80
118
|
|
|
81
|
-
|
|
119
|
+
@classmethod
|
|
120
|
+
def _build_body(cls, context) -> Tuple[ca.Element, List[ca.Answer]]:
|
|
82
121
|
try:
|
|
83
|
-
|
|
122
|
+
generator_fn = context.get("generator_fn")
|
|
123
|
+
if generator_fn is None:
|
|
124
|
+
raise TypeError("No generator provided for FromGenerator.")
|
|
125
|
+
generated_content = generator_fn(context.get("generator_scope"))
|
|
84
126
|
if isinstance(generated_content, ca.Section):
|
|
85
127
|
body = generated_content
|
|
86
128
|
elif isinstance(generated_content, str):
|