QuizGenerator 0.8.0__tar.gz → 0.9.0__tar.gz

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 (58) hide show
  1. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/PKG-INFO +9 -6
  2. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/contentast.py +43 -10
  3. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/generate.py +1 -1
  4. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/mixins.py +6 -2
  5. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/basic.py +49 -7
  6. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/process.py +1 -7
  7. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +92 -82
  8. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +68 -45
  9. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +235 -162
  10. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +0 -1
  11. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +51 -45
  12. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +212 -215
  13. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/question.py +176 -18
  14. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/README.md +8 -5
  15. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/pyproject.toml +1 -1
  16. quizgenerator-0.9.0/pyproject_prev.toml +76 -0
  17. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/uv.lock +1 -1
  18. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/.envrc +0 -0
  19. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/.gitignore +0 -0
  20. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/CODEOWNERS +0 -0
  21. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/LICENSE +0 -0
  22. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/__init__.py +0 -0
  23. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/__main__.py +0 -0
  24. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/canvas/__init__.py +0 -0
  25. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/canvas/canvas_interface.py +0 -0
  26. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/canvas/classes.py +0 -0
  27. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/constants.py +0 -0
  28. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/misc.py +0 -0
  29. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/performance.py +0 -0
  30. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/__init__.py +0 -0
  31. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/__init__.py +0 -0
  32. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/languages.py +0 -0
  33. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/math_questions.py +0 -0
  34. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/memory_questions.py +0 -0
  35. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +0 -0
  36. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/persistence_questions.py +0 -0
  37. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/__init__.py +0 -0
  38. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +0 -0
  39. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +0 -0
  40. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +0 -0
  41. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +0 -0
  42. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
  43. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/attention.py +0 -0
  44. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/cnns.py +0 -0
  45. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/matrices.py +0 -0
  46. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/rnns.py +0 -0
  47. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/text.py +0 -0
  48. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/weight_counting.py +0 -0
  49. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +0 -0
  50. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +0 -0
  51. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/qrcode_generator.py +0 -0
  52. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/quiz.py +0 -0
  53. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/regenerate.py +0 -0
  54. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/QuizGenerator/typst_utils.py +0 -0
  55. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/examples/web_ui_integration_example.py +0 -0
  56. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/scripts/generate_practice_yaml.sh +0 -0
  57. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/scripts/print.sh +0 -0
  58. {quizgenerator-0.8.0 → quizgenerator-0.9.0}/scripts/vendor_lms_interface.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuizGenerator
3
- Version: 0.8.0
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
@@ -668,7 +668,7 @@ class Section(Container):
668
668
  - Organizing complex question content
669
669
 
670
670
  Example:
671
- def _build_body(self, context):
671
+ def _build_body(cls, context):
672
672
  body = Section()
673
673
  answers = []
674
674
  body.add_element(Paragraph(["Calculate the determinant:"]))
@@ -2263,6 +2263,10 @@ class Answer(Leaf):
2263
2263
  unit_part = f" {self.unit}" if self.unit else ""
2264
2264
 
2265
2265
  return f"{label_part} {blank}{unit_part}".strip()
2266
+
2267
+ @classmethod
2268
+ def get_entry_warning(cls) -> List[str] | None:
2269
+ return None
2266
2270
 
2267
2271
  # Factory methods for common answer types
2268
2272
  @classmethod
@@ -2330,17 +2334,19 @@ class Answer(Leaf):
2330
2334
  0.123444... → ["0.1234"]
2331
2335
  """
2332
2336
  rounding_digits = Answer.DEFAULT_ROUNDING_DIGITS
2333
- decimal.getcontext().prec = max(34, rounding_digits + 10)
2334
-
2335
2337
  outs = set()
2336
2338
 
2337
2339
  # Round to our standard precision first
2340
+ decimal.getcontext().prec = max(34, rounding_digits + 10)
2338
2341
  q = decimal.Decimal(1).scaleb(-rounding_digits)
2339
- rounded_decimal = decimal.Decimal(str(value)).quantize(q, rounding=decimal.ROUND_HALF_UP)
2340
-
2341
- # Normalize negative zero to positive zero
2342
- if rounded_decimal == 0:
2343
- rounded_decimal = abs(rounded_decimal)
2342
+ if isinstance(value, str) and '/' in value:
2343
+ f = Answer._to_fraction(value)
2344
+ decimal_value = decimal.Decimal(f.numerator) / decimal.Decimal(f.denominator)
2345
+ elif isinstance(value, fractions.Fraction):
2346
+ decimal_value = decimal.Decimal(value.numerator) / decimal.Decimal(value.denominator)
2347
+ else:
2348
+ decimal_value = decimal.Decimal(str(value))
2349
+ rounded_decimal = decimal_value.quantize(q, rounding=decimal.ROUND_HALF_UP)
2344
2350
 
2345
2351
  # Fixed decimal form (e.g., "1.2500")
2346
2352
  fixed_str = format(rounded_decimal, 'f')
@@ -2431,6 +2437,15 @@ class AnswerTypes:
2431
2437
 
2432
2438
  # Concrete type answers
2433
2439
  class Float(Answer):
2440
+ @classmethod
2441
+ def get_entry_warning(cls) -> List[str] | None:
2442
+ digits = Answer.DEFAULT_ROUNDING_DIGITS
2443
+ return [
2444
+ f"Round floats to {digits} decimal places (fewer if exact, e.g., `1.25`). "
2445
+ "No mixed numbers (e.g. use `5/4`, not `1 1/4`). "
2446
+ "Integers as integers (e.g., `2`, not `2/1`)."
2447
+ ]
2448
+
2434
2449
  def get_for_canvas(self, single_answer=False) -> List[dict]:
2435
2450
  if single_answer:
2436
2451
  canvas_answers = [
@@ -2460,8 +2475,8 @@ class AnswerTypes:
2460
2475
  return canvas_answers
2461
2476
 
2462
2477
  def get_display_string(self) -> str:
2463
- rounded = round(self.value, Answer.DEFAULT_ROUNDING_DIGITS)
2464
- return f"{self.fix_negative_zero(rounded)}"
2478
+ answer_strings = Answer.accepted_strings(self.value)
2479
+ return answer_strings[0] if len(answer_strings) > 0 else f"{self.value}"
2465
2480
 
2466
2481
  class Int(Answer):
2467
2482
 
@@ -2486,6 +2501,10 @@ class AnswerTypes:
2486
2501
  pass
2487
2502
 
2488
2503
  class List(Answer):
2504
+ @classmethod
2505
+ def get_entry_warning(cls) -> List[str] | None:
2506
+ return ["Enter lists as comma-separated values with a space after the comma (e.g., `1, 2, 3`)."]
2507
+
2489
2508
  def __init__(self, order_matters=True, *args, **kwargs):
2490
2509
  super().__init__(*args, **kwargs)
2491
2510
  self.order_matters = order_matters
@@ -2523,6 +2542,13 @@ class AnswerTypes:
2523
2542
  """
2524
2543
  These are self-contained vectors that will go in a single answer block
2525
2544
  """
2545
+
2546
+ @classmethod
2547
+ def get_entry_warning(cls) -> List[str] | None:
2548
+ return [
2549
+ "Enter vectors as comma-separated values with a space after the comma, "
2550
+ "with optional parentheses (e.g., `1, 2` or `(1, 2)`)."
2551
+ ]
2526
2552
 
2527
2553
  # Canvas export methods (from misc.Answer)
2528
2554
  def get_for_canvas(self, single_answer=False) -> List[dict]:
@@ -2565,6 +2591,13 @@ class AnswerTypes:
2565
2591
  """
2566
2592
  Matrix answers generate multiple blank_ids (e.g., M_0_0, M_0_1, M_1_0, M_1_1).
2567
2593
  """
2594
+
2595
+ @classmethod
2596
+ def get_entry_warning(cls) -> List[str] | None:
2597
+ return [
2598
+ "For result matrices, enter `-` in any cell that does not exist.",
2599
+ *AnswerTypes.Float.get_entry_warning()
2600
+ ]
2568
2601
 
2569
2602
  def __init__(self, value, *args, **kwargs):
2570
2603
  super().__init__(value=value, *args, **kwargs)
@@ -153,7 +153,7 @@ def test_all_questions(
153
153
  )
154
154
 
155
155
  # Generate the question (this calls refresh and builds the AST)
156
- instance = question.instantiate(rng_seed=seed)
156
+ instance = question.instantiate(rng_seed=seed, max_backoff_attempts=200)
157
157
  question_ast = question._build_question_ast(instance)
158
158
 
159
159
  # Try rendering to both formats to catch format-specific issues
@@ -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
- def _build_body(self, context):
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
- def _build_explanation(self, context):
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
- def _build_context(self, *, rng_seed=None, **kwargs):
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"] = self.text
29
+ context["text"] = kwargs.get("text", "")
27
30
  return context
28
31
 
29
- def _build_body(self, context) -> Tuple[ca.Element, List[ca.Answer]]:
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
- def _build_explanation(self, context) -> Tuple[ca.Element, List[ca.Answer]]:
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
- def _build_context(self, *, rng_seed=None, **kwargs):
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
- def _build_body(self, context) -> Tuple[ca.Element, List[ca.Answer]]:
119
+ @classmethod
120
+ def _build_body(cls, context) -> Tuple[ca.Element, List[ca.Answer]]:
82
121
  try:
83
- generated_content = self.generator()
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):
@@ -433,13 +433,7 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
433
433
  f"Break any ties using the job number."
434
434
  )
435
435
 
436
- instructions = ca.OnlyHtml([ca.Paragraph([
437
- f"Please format answer as fractions, mixed numbers, or numbers rounded to a maximum of {ca.Answer.DEFAULT_ROUNDING_DIGITS} digits after the decimal. "
438
- "Examples of appropriately formatted answers would be `0`, `3/2`, `1 1/3`, `1.6667`, and `1.25`. "
439
- "Note that answers that can be rounded to whole numbers should be, rather than being left in fractional form."
440
- ])])
441
-
442
- body = cls.create_fill_in_table_body(intro_text, instructions, scheduling_table)
436
+ body = cls.create_fill_in_table_body(intro_text, None, scheduling_table)
443
437
  body.add_element(average_block)
444
438
  return body
445
439