Skip to content

Commit d5929f0

Browse files
committed
feat(question-api): improve main_text encapsulation
- Fix anti-pattern of private `_main_text` in public Question dataclass (https://stackoverflow.com/a/61480946). - Also allow sphinx to find main_text examples. BREAKING CHANGE: During Question initialisation, `_main_text` attribute replaced with `main_text`.
1 parent 41d8be2 commit d5929f0

File tree

3 files changed

+44
-24
lines changed

3 files changed

+44
-24
lines changed

in2lambda/api/module.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ def current_question(self) -> Question:
2929
Examples:
3030
>>> from in2lambda.api.module import Module
3131
>>> Module().current_question
32-
Question(title='INVALID', parts=[], images=[], _main_text='')
32+
Question(title='INVALID', parts=[], images=[], main_text='')
3333
>>> module = Module()
3434
>>> module.add_question()
3535
>>> module.current_question
36-
Question(title='', parts=[], images=[], _main_text='')
36+
Question(title='', parts=[], images=[], main_text='')
3737
"""
3838
return (
3939
self.questions[self._current_question_index]
@@ -57,7 +57,7 @@ def add_question(
5757
>>> module = Module()
5858
>>> module.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there")))
5959
>>> module
60-
Module(questions=[Question(title='Some title', parts=[], images=[], _main_text='hello there')])
60+
Module(questions=[Question(title='Some title', parts=[], images=[], main_text='hello there')])
6161
>>> module.add_question(main_text="Normal string text")
6262
>>> module.questions[1].main_text
6363
'Normal string text'
@@ -86,8 +86,8 @@ def increment_current_question(self) -> None:
8686
>>> module.increment_current_question()
8787
>>> module.current_question.add_solution("Question 2 answer")
8888
>>> module.questions
89-
[Question(title='Question 1', parts=[Part(text='', worked_solution='Question 1 answer')], images=[], _main_text=''),\
90-
Question(title='Question 2', parts=[Part(text='', worked_solution='Question 2 answer')], images=[], _main_text='')]
89+
[Question(title='Question 1', parts=[Part(text='', worked_solution='Question 1 answer')], images=[], main_text=''),\
90+
Question(title='Question 2', parts=[Part(text='', worked_solution='Question 2 answer')], images=[], main_text='')]
9191
"""
9292
self._current_question_index += 1
9393

in2lambda/api/question.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,20 @@ class Question:
1616
1717
Examples:
1818
>>> from in2lambda.api.question import Question
19-
>>> Question(title="Some title", _main_text="Some text")
20-
Question(title='Some title', parts=[], images=[], _main_text='Some text')
19+
>>> Question(title="Some title", main_text="Some text")
20+
Question(title='Some title', parts=[], images=[], main_text='Some text')
2121
"""
2222

2323
title: str = ""
2424

2525
parts: list[Part] = field(default_factory=list)
2626
images: list[str] = field(default_factory=list)
2727

28-
_main_text: str = ""
28+
# Use getter/setter methods for main_text without exposing private attribute.
29+
# See https://stackoverflow.com/a/61480946
30+
main_text: str = ""
31+
_main_text: str = field(init=False, repr=False, default="")
32+
2933
_last_part: dict[str, int] = field(
3034
default_factory=lambda: {"solution": 0, "text": 0}, repr=False
3135
)
@@ -34,15 +38,10 @@ class Question:
3438

3539
@property
3640
def main_text(self) -> str:
37-
"""Main top-level question text."""
38-
return self._main_text
41+
r"""Main top-level question text.
3942
40-
@main_text.setter
41-
def main_text(self, value: Union[pf.Element, str]) -> None:
42-
r"""Appends to the top-level main text, which starts off as an empty string.
43-
44-
Args:
45-
value: A panflute element or string denoting what to append to the main text.
43+
Setting the attribute multiple times appends to the current value with a newline.
44+
This allows question text to be dynamically appended.
4645
4746
Examples:
4847
>>> from in2lambda.api.question import Question
@@ -52,7 +51,28 @@ def main_text(self, value: Union[pf.Element, str]) -> None:
5251
>>> question.main_text
5352
'hello\nthere'
5453
"""
55-
text_value = value if isinstance(value, str) else pf.stringify(value, False)
54+
return self._main_text
55+
56+
@main_text.setter
57+
def main_text(self, value: Union[pf.Element, str, property]) -> None:
58+
r"""Appends to the top-level main text, which starts off as an empty string.
59+
60+
Args:
61+
value: A panflute element or string denoting what to append to the main text.
62+
63+
See example in main_text property.
64+
"""
65+
# Converts the inputted value into a string, stored in text_value
66+
match value:
67+
case str():
68+
text_value = value
69+
case property():
70+
# Use default value when no value set at initialisation.
71+
# See https://stackoverflow.com/a/61480946
72+
text_value = self.main_text
73+
case pf.Element():
74+
text_value = pf.stringify(value, False)
75+
5676
if self._main_text:
5777
self._main_text += "\n"
5878
self._main_text += text_value
@@ -69,20 +89,20 @@ def add_solution(self, elem: Union[pf.Element, str]) -> None:
6989
>>> question.add_part_text("part a")
7090
>>> question.add_solution("part a solution")
7191
>>> question
72-
Question(title='', parts=[Part(text='part a', worked_solution='part a solution')], images=[], _main_text='')
92+
Question(title='', parts=[Part(text='part a', worked_solution='part a solution')], images=[], main_text='')
7393
>>> question.add_part_text("part b")
7494
>>> question.add_part_text("part c")
7595
>>> question.add_solution("Solution for b")
7696
>>> # Note that since c doesn't have a solution, it's set to b's solution
7797
>>> question
7898
Question(title='', parts=[Part(text='part a', worked_solution='part a solution'), \
7999
Part(text='part b', worked_solution='Solution for b'), \
80-
Part(text='part c', worked_solution='Solution for b')], images=[], _main_text='')
100+
Part(text='part c', worked_solution='Solution for b')], images=[], main_text='')
81101
>>> question.add_solution("We now have a solution for c!")
82102
>>> question
83103
Question(title='', parts=[Part(text='part a', worked_solution='part a solution'), \
84104
Part(text='part b', worked_solution='Solution for b'), \
85-
Part(text='part c', worked_solution='We now have a solution for c!')], images=[], _main_text='')
105+
Part(text='part c', worked_solution='We now have a solution for c!')], images=[], main_text='')
86106
"""
87107
elem_text = elem if isinstance(elem, str) else pf.stringify(elem)
88108

@@ -111,13 +131,13 @@ def add_part_text(self, elem: Union[pf.Element, str]) -> None:
111131
>>> question.add_part_text("part a")
112132
>>> question.add_solution("part a solution")
113133
>>> question
114-
Question(title='', parts=[Part(text='part a', worked_solution='part a solution')], images=[], _main_text='')
134+
Question(title='', parts=[Part(text='part a', worked_solution='part a solution')], images=[], main_text='')
115135
>>> # Supports adding the answer first.
116136
>>> question.add_solution("part b solution")
117137
>>> question.add_part_text("part b")
118138
>>> question
119139
Question(title='', parts=[Part(text='part a', worked_solution='part a solution'), \
120-
Part(text='part b', worked_solution='part b solution')], images=[], _main_text='')
140+
Part(text='part b', worked_solution='part b solution')], images=[], main_text='')
121141
"""
122142
elem_text = elem if isinstance(elem, str) else pf.stringify(elem)
123143

in2lambda/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ def runner(
8080
>>> from in2lambda.main import runner
8181
>>> # Retrieve an example TeX file and run the given filter.
8282
>>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsSepSol/example.tex", "PartsSepSol") # doctest: +ELLIPSIS
83-
Module(questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], _main_text='This is a sample question\n\n'), ...])
83+
Module(questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='This is a sample question\n\n'), ...])
8484
>>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsOneSol/example.tex", "PartsOneSol") # doctest: +ELLIPSIS
85-
Module(questions=[Question(title='', parts=[Part(text='This is part (a)\n\n', worked_solution=''), ...], images=[], _main_text='Here is some preliminary question information that might be useful.'), ...)
85+
Module(questions=[Question(title='', parts=[Part(text='This is part (a)\n\n', worked_solution=''), ...], images=[], main_text='Here is some preliminary question information that might be useful.'), ...)
8686
"""
8787
# The list of questions for Lambda Feedback as a Python API.
8888
module = Module()

0 commit comments

Comments
 (0)