QuizGenerator 0.8.1__py3-none-any.whl → 0.9.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/question.py CHANGED
@@ -15,10 +15,13 @@ import pprint
15
15
  import random
16
16
  import re
17
17
  import uuid
18
+ import types
19
+ import inspect
20
+ from types import MappingProxyType
18
21
 
19
22
  import pypandoc
20
23
  import yaml
21
- from typing import List, Dict, Any, Tuple, Optional
24
+ from typing import List, Dict, Any, Tuple, Optional, Mapping, MutableMapping
22
25
  import canvasapi.course, canvasapi.quiz
23
26
 
24
27
  import QuizGenerator.contentast as ca
@@ -58,6 +61,81 @@ class QuestionInstance:
58
61
  flags: RegenerationFlags
59
62
 
60
63
 
64
+ @dataclasses.dataclass
65
+ class QuestionContext:
66
+ rng_seed: Optional[int]
67
+ rng: random.Random
68
+ data: MutableMapping[str, Any] | Mapping[str, Any] = dataclasses.field(default_factory=dict)
69
+ frozen: bool = False
70
+ question_cls: type | None = None
71
+
72
+ def __getitem__(self, key: str) -> Any:
73
+ if key == "rng_seed":
74
+ return self.rng_seed
75
+ if key == "rng":
76
+ return self.rng
77
+ return self.data[key]
78
+
79
+ def __setitem__(self, key: str, value: Any) -> None:
80
+ if self.frozen:
81
+ raise TypeError("QuestionContext is frozen.")
82
+ if key == "rng_seed":
83
+ self.rng_seed = value
84
+ return
85
+ if key == "rng":
86
+ self.rng = value
87
+ return
88
+ if isinstance(self.data, MappingProxyType):
89
+ raise TypeError("QuestionContext is frozen.")
90
+ self.data[key] = value
91
+
92
+ def get(self, key: str, default: Any = None) -> Any:
93
+ if key == "rng_seed":
94
+ return self.rng_seed
95
+ if key == "rng":
96
+ return self.rng
97
+ return self.data.get(key, default)
98
+
99
+ def __contains__(self, key: object) -> bool:
100
+ if key in ("rng_seed", "rng"):
101
+ return True
102
+ return key in self.data
103
+
104
+ def __getattr__(self, name: str) -> Any:
105
+ if name in self.data:
106
+ return self.data[name]
107
+ if self.question_cls is not None and hasattr(self.question_cls, name):
108
+ raw_attr = inspect.getattr_static(self.question_cls, name)
109
+ if isinstance(raw_attr, staticmethod):
110
+ return raw_attr.__func__
111
+ if isinstance(raw_attr, classmethod):
112
+ return raw_attr.__func__.__get__(self.question_cls, self.question_cls)
113
+ attr = getattr(self.question_cls, name)
114
+ if callable(attr):
115
+ return types.MethodType(attr, self)
116
+ return attr
117
+ raise AttributeError(f"{type(self).__name__} has no attribute {name!r}")
118
+
119
+ def keys(self):
120
+ return self.data.keys()
121
+
122
+ def items(self):
123
+ return self.data.items()
124
+
125
+ def values(self):
126
+ return self.data.values()
127
+
128
+ def freeze(self) -> "QuestionContext":
129
+ frozen_data = MappingProxyType(dict(self.data))
130
+ return QuestionContext(
131
+ rng_seed=self.rng_seed,
132
+ rng=self.rng,
133
+ data=frozen_data,
134
+ frozen=True,
135
+ question_cls=self.question_cls,
136
+ )
137
+
138
+
61
139
  # Spacing presets for questions
62
140
  SPACING_PRESETS = {
63
141
  "NONE": 0,
@@ -262,7 +340,7 @@ class RegenerableChoiceMixin:
262
340
  self.register_choice('scheduler_kind', self.Kind, scheduler_kind, kwargs)
263
341
  super().__init__(**kwargs)
264
342
 
265
- def _build_context(self, rng_seed=None, **kwargs):
343
+ def _build_context(cls, rng_seed=None, **kwargs):
266
344
  self.rng.seed(rng_seed)
267
345
  # Get the choice (randomly selected or from config_params)
268
346
  self.scheduler_algorithm = self.get_choice('scheduler_kind', self.Kind)
@@ -439,6 +517,7 @@ class Question(abc.ABC):
439
517
 
440
518
  # Default version - subclasses should override this
441
519
  VERSION = "1.0"
520
+ FREEZE_CONTEXT = False
442
521
 
443
522
  class Topic(enum.Enum):
444
523
  # CST334 (Operating Systems) Topics
@@ -547,8 +626,10 @@ class Question(abc.ABC):
547
626
  """
548
627
  # Generate the question, retrying with incremented seeds until we get an interesting one
549
628
  base_seed = kwargs.get("rng_seed", None)
629
+ max_backoff_attempts = kwargs.get("max_backoff_attempts", None)
550
630
  build_kwargs = dict(kwargs)
551
631
  build_kwargs.pop("rng_seed", None)
632
+ build_kwargs.pop("max_backoff_attempts", None)
552
633
  # Include config params so build() implementations can access YAML-provided settings.
553
634
  build_kwargs = {**self.config_params, **build_kwargs}
554
635
 
@@ -562,6 +643,10 @@ class Question(abc.ABC):
562
643
  is_interesting = False
563
644
  ctx = None
564
645
  while not is_interesting:
646
+ if max_backoff_attempts is not None and backoff_counter >= max_backoff_attempts:
647
+ raise RuntimeError(
648
+ f"Exceeded max_backoff_attempts={max_backoff_attempts} for {self.__class__.__name__}"
649
+ )
565
650
  # Increment seed for each backoff attempt to maintain deterministic behavior
566
651
  current_seed = None if base_seed is None else base_seed + backoff_counter
567
652
  ctx = self._build_context(
@@ -574,7 +659,17 @@ class Question(abc.ABC):
574
659
  # Store the actual seed used and question metadata for QR code generation
575
660
  actual_seed = None if base_seed is None else base_seed + backoff_counter - 1
576
661
 
577
- components = self.build(rng_seed=current_seed, context=ctx, **build_kwargs)
662
+ # Keep instance rng in sync for any legacy usage.
663
+ if isinstance(ctx, QuestionContext):
664
+ self.rng = ctx.rng
665
+ elif isinstance(ctx, dict) and "rng" in ctx:
666
+ self.rng = ctx["rng"]
667
+
668
+ components = self.__class__.build(
669
+ rng_seed=current_seed,
670
+ context=ctx,
671
+ **build_kwargs
672
+ )
578
673
 
579
674
  # Collect answers from explicit lists and inline AST
580
675
  inline_body_answers = self._collect_answers_from_ast(components.body)
@@ -619,19 +714,27 @@ class Question(abc.ABC):
619
714
  def post_instantiate(self, instance, **kwargs):
620
715
  pass
621
716
 
622
- def build(self, *, rng_seed=None, context=None, **kwargs) -> QuestionComponents:
717
+ @classmethod
718
+ def build(cls, *, rng_seed=None, context=None, **kwargs) -> QuestionComponents:
623
719
  """
624
720
  Build question content (body, answers, explanation) for a given seed.
625
721
 
626
722
  This should only generate content; metadata like points/spacing belong in instantiate().
627
723
  """
628
- cls = self.__class__
629
724
  if context is None:
630
- context = self._build_context(rng_seed=rng_seed, **kwargs)
725
+ context = cls._coerce_context(
726
+ cls._build_context(rng_seed=rng_seed, **kwargs),
727
+ rng_seed=rng_seed
728
+ )
729
+ else:
730
+ context = cls._coerce_context(context, rng_seed=rng_seed)
731
+
732
+ if cls.FREEZE_CONTEXT:
733
+ context = context.freeze()
631
734
 
632
735
  # Build body + explanation. Each may return just an Element or (Element, answers).
633
- body, body_answers = cls._normalize_build_output(self._build_body(context))
634
- explanation, explanation_answers = cls._normalize_build_output(self._build_explanation(context))
736
+ body, body_answers = cls._normalize_build_output(cls._build_body(context))
737
+ explanation, explanation_answers = cls._normalize_build_output(cls._build_explanation(context))
635
738
 
636
739
  # Collect inline answers from both body and explanation.
637
740
  inline_body_answers = cls._collect_answers_from_ast(body)
@@ -650,30 +753,48 @@ class Question(abc.ABC):
650
753
  explanation=explanation
651
754
  )
652
755
 
653
- def _build_context(self, *, rng_seed=None, **kwargs):
756
+ @classmethod
757
+ def _coerce_context(cls, context, *, rng_seed=None) -> QuestionContext:
758
+ if isinstance(context, QuestionContext):
759
+ return context
760
+ if isinstance(context, dict):
761
+ ctx_seed = context.get("rng_seed", rng_seed)
762
+ rng = context.get("rng") or random.Random(ctx_seed)
763
+ ctx = QuestionContext(rng_seed=ctx_seed, rng=rng)
764
+ for key, value in context.items():
765
+ if key in ("rng_seed", "rng"):
766
+ continue
767
+ ctx.data[key] = value
768
+ return ctx
769
+ raise TypeError(f"Unsupported context type: {type(context)}")
770
+
771
+
772
+ @classmethod
773
+ def _build_context(cls, *, rng_seed=None, **kwargs) -> QuestionContext:
654
774
  """
655
775
  Build the deterministic context for a question instance.
656
776
 
657
- Override to return a context dict and avoid persistent self.* state.
777
+ Override to return a QuestionContext and avoid persistent self.* state.
658
778
  """
659
779
  rng = random.Random(rng_seed)
660
- # Keep instance rng in sync for questions that still use self.rng.
661
- self.rng = rng
662
- return {
663
- "rng_seed": rng_seed,
664
- "rng": rng,
665
- }
780
+ return QuestionContext(
781
+ rng_seed=rng_seed,
782
+ rng=rng,
783
+ question_cls=cls,
784
+ )
666
785
 
667
786
  @classmethod
668
787
  def is_interesting_ctx(cls, context) -> bool:
669
788
  """Context-aware hook; defaults to existing is_interesting()."""
670
789
  return True
671
790
 
672
- def _build_body(self, context) -> ca.Element | Tuple[ca.Element, List[ca.Answer]]:
791
+ @classmethod
792
+ def _build_body(cls, context) -> ca.Element | Tuple[ca.Element, List[ca.Answer]]:
673
793
  """Context-aware body builder."""
674
794
  raise NotImplementedError("Questions must implement _build_body().")
675
795
 
676
- def _build_explanation(self, context) -> ca.Element | Tuple[ca.Element, List[ca.Answer]]:
796
+ @classmethod
797
+ def _build_explanation(cls, context) -> ca.Element | Tuple[ca.Element, List[ca.Answer]]:
677
798
  """Context-aware explanation builder."""
678
799
  raise NotImplementedError("Questions must implement _build_explanation().")
679
800
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuizGenerator
3
- Version: 0.8.1
3
+ Version: 0.9.0
4
4
  Summary: Generate randomized quiz questions for Canvas LMS and PDF exams
5
5
  Project-URL: Homepage, https://github.com/OtterDen-Lab/QuizGenerator
6
6
  Project-URL: Documentation, https://github.com/OtterDen-Lab/QuizGenerator/tree/main/documentation
@@ -150,26 +150,29 @@ All questions follow the same three‑method flow:
150
150
 
151
151
  ```python
152
152
  class MyQuestion(Question):
153
- def _build_context(self, *, rng_seed=None, **kwargs):
153
+ @classmethod
154
+ def _build_context(cls, *, rng_seed=None, **kwargs):
154
155
  context = super()._build_context(rng_seed=rng_seed, **kwargs)
155
- rng = context["rng"]
156
+ rng = context.rng
156
157
  context["value"] = rng.randint(1, 10)
157
158
  return context
158
159
 
159
- def _build_body(self, context):
160
+ @classmethod
161
+ def _build_body(cls, context):
160
162
  body = ca.Section()
161
163
  body.add_element(ca.Paragraph([f"Value: {context['value']}"]))
162
164
  body.add_element(ca.AnswerTypes.Int(context["value"], label="Value"))
163
165
  return body
164
166
 
165
- def _build_explanation(self, context):
167
+ @classmethod
168
+ def _build_explanation(cls, context):
166
169
  explanation = ca.Section()
167
170
  explanation.add_element(ca.Paragraph([f"Answer: {context['value']}"]))
168
171
  return explanation
169
172
  ```
170
173
 
171
174
  Notes:
172
- - Always use `context["rng"]` for deterministic randomness.
175
+ - Always use `context.rng` (or `context["rng"]`) for deterministic randomness.
173
176
  - Avoid `refresh()`; it is no longer part of the API.
174
177
 
175
178
  ## Built-in Question Types
@@ -1,13 +1,13 @@
1
1
  QuizGenerator/__init__.py,sha256=8EV-k90A3PNC8Cm2-ZquwNyVyvnwW1gs6u-nGictyhs,840
2
2
  QuizGenerator/__main__.py,sha256=Dd9w4R0Unm3RiXztvR4Y_g9-lkWp6FHg-4VN50JbKxU,151
3
3
  QuizGenerator/constants.py,sha256=AO-UWwsWPLb1k2JW6KP8rl9fxTcdT0rW-6XC6zfnDOs,4386
4
- QuizGenerator/contentast.py,sha256=uTql3nvNg8DZnPxOg7S31vXbifSNfb3rFK5c_ihLsbg,87615
5
- QuizGenerator/generate.py,sha256=dqF-WWmWxyJmPHl0gTYr3gNNxyF877fvXYaMvYA3uA8,15790
4
+ QuizGenerator/contentast.py,sha256=LEjr-J79ooge0sAlZMuJcyz5Xfj2wRHlAJ_7jAULhBY,87614
5
+ QuizGenerator/generate.py,sha256=qXLJ3WfOo_poIWoAZvEK7epNlVNSWxpOomMVt2MDExA,15816
6
6
  QuizGenerator/misc.py,sha256=MXrguUhhdrWSV4Hqdl4G21ktowODu1AcKy6-5mvy3aI,454
7
- QuizGenerator/mixins.py,sha256=B9Ee52wUCeclmBTgonasHNo0WHvVOcnILsz0iecrf78,15705
7
+ QuizGenerator/mixins.py,sha256=zXj2U94qNbIEusbwTnzRM1Z_zSybpvozWhveq-t5q2Q,15771
8
8
  QuizGenerator/performance.py,sha256=CM3zLarJXN5Hfrl4-6JRBqD03j4BU1B2QW699HAr1Ds,7002
9
9
  QuizGenerator/qrcode_generator.py,sha256=S3mzZDk2UiHiw6ipSCpWPMhbKvSRR1P5ordZJUTo6ug,10776
10
- QuizGenerator/question.py,sha256=QsLKFEM8LzLkH1_5MOwMFRuqtTkEd7-a_eoaTKcttpU,33602
10
+ QuizGenerator/question.py,sha256=PKpQ6ZsHkyNw3yJBXlU3akS9Dqhmprq0dLu7wjRzg9A,37240
11
11
  QuizGenerator/quiz.py,sha256=CEWy7FB7BZiK33s_wYs6MqGKDetc6htUaqvP3--2HzI,21621
12
12
  QuizGenerator/regenerate.py,sha256=ZAs1mtERmO8JXza2tBqJpd-uJs9V7gS1jJ9A9gSb8jo,19764
13
13
  QuizGenerator/typst_utils.py,sha256=JGQn_u5bEHd8HAtjAHuZoVJwLkx-Rd4ZCBWffwFZa3o,3136
@@ -15,7 +15,7 @@ QuizGenerator/canvas/__init__.py,sha256=TwFP_zgxPIlWtkvIqQ6mcvBNTL9swIH_rJl7DGKc
15
15
  QuizGenerator/canvas/canvas_interface.py,sha256=StMcdXgLvTA1EayQ44m_le2GXGQpDQnduYXVeUYsqW0,24618
16
16
  QuizGenerator/canvas/classes.py,sha256=v_tQ8t_JJplU9sv2p4YctX45Fwed1nQ2HC1oC9BnDNw,7594
17
17
  QuizGenerator/premade_questions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- QuizGenerator/premade_questions/basic.py,sha256=F_Qsu14b2xERyibCAMP1NPgnmi1h2G2DDkKJoU_RS8g,3284
18
+ QuizGenerator/premade_questions/basic.py,sha256=u6Viv__5HYWjLOknYt_jsBJTej5-cPd9FM1xsuwUQcQ,4613
19
19
  QuizGenerator/premade_questions/cst334/__init__.py,sha256=BTz-Os1XbwIRKqAilf2UIva2NlY0DbA_XbSIggO2Tdk,36
20
20
  QuizGenerator/premade_questions/cst334/languages.py,sha256=ctemEAMkI8C6ASMIf59EHAW1ndFWi7hXzdEt-zOByUE,14114
21
21
  QuizGenerator/premade_questions/cst334/math_questions.py,sha256=aUYbQxneL5MXE7Xo3tZX9-xcg71CXwvG3rrxcoh0l7A,8638
@@ -25,9 +25,9 @@ QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=9mgsX-3oW
25
25
  QuizGenerator/premade_questions/cst334/process.py,sha256=0SqXkvdxaEJZXJA8fBqheh7F0PL8I6xgO5a8u2s2po4,37238
26
26
  QuizGenerator/premade_questions/cst463/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py,sha256=sH2CUV6zK9FT3jWTn453ys6_JTrUKRtZnU8hK6RmImU,240
28
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=laBeC0tMc2EaLzCotGHQNzePPPOKS1EGgQirigNyi9M,13479
29
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py,sha256=_2HlC0sqfGy__Qyzbw0PwMD_OkQiiQukl72LQFyPYx0,10939
30
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py,sha256=8GtJX1DNNox-AgMvABFkRgmHB-lvrxMZKzv-3Ils_Jg,22380
28
+ QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=y_R26wUt7AIZVUoe3e_qpzkPdwMyo3mZjxLI504-YQw,13840
29
+ QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py,sha256=OssybIkHx4l8ryHGT9rqHUecta9qpItK4QYVHuuQLeo,11525
30
+ QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py,sha256=slNXfEeLRRM8IDhn3TSQdUWjAimDIfbgUSEmb48zCuU,24156
31
31
  QuizGenerator/premade_questions/cst463/gradient_descent/misc.py,sha256=0R-nFeD3zsqJyde5CXWrF6Npjmpx6_HbzfCbThLi3os,2657
32
32
  QuizGenerator/premade_questions/cst463/math_and_data/__init__.py,sha256=EbIaUrx7_aK9j3Gd8Mk08h9GocTq_0OoNu2trfNwaU8,202
33
33
  QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py,sha256=4DLdo_8XDS_xtPA8R-wH4K0cKnMn4r5727Vszz8keTc,15565
@@ -40,11 +40,11 @@ QuizGenerator/premade_questions/cst463/models/rnns.py,sha256=5fKQuWnpSAoznZVJuCY
40
40
  QuizGenerator/premade_questions/cst463/models/text.py,sha256=BnW6qIB8pnQiFRyXxtX9cdsIfmjw99p6TI0WqI0AQzk,6605
41
41
  QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=sFnEvSs7ZwR4RZPltiMEElKJgoxHTaY427_g8Abi2uk,6912
42
42
  QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py,sha256=pmyCezO-20AFEQC6MR7KnAsaU9TcgZYsGQOMVkRZ-U8,149
43
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=bit_HfAG4K6yh9SZZw_HAPhFUVFkOBdZ2odwt-Cdvmo,42868
43
+ QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=j2f5LFmme-2rSgJzcb8nZJ1_hnZaL-S4lXSnIbpoH_E,43010
44
44
  QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py,sha256=G1gEHtG4KakYgi8ZXSYYhX6bQRtnm2tZVGx36d63Nmo,173
45
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=jZRbEqb65BAtliv_V9VR4kvpwOt-o10ApN7RmOIg3XI,30464
46
- quizgenerator-0.8.1.dist-info/METADATA,sha256=Ef_TPUm2UKYIXnBMiaip6SaAyRjMD4G1MNdVejagfRw,8113
47
- quizgenerator-0.8.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
- quizgenerator-0.8.1.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
49
- quizgenerator-0.8.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
50
- quizgenerator-0.8.1.dist-info/RECORD,,
45
+ QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=t0ghv6o8AsZaIwVHFu07Ozebwuv6Rf18D3g_-Jz4O74,31309
46
+ quizgenerator-0.9.0.dist-info/METADATA,sha256=84oI93hpRcDQO03t5qMjloNUWcCQo9MgOZmkMe9YrbY,8177
47
+ quizgenerator-0.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
+ quizgenerator-0.9.0.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
49
+ quizgenerator-0.9.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
50
+ quizgenerator-0.9.0.dist-info/RECORD,,