From b2e78270a8a950b0863dabbcbc22c2185874c03c Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 28 Jul 2025 16:26:56 +0100 Subject: [PATCH 01/22] added ordering to the questions --- .metals/metals.lock.db | 6 ++++++ in2lambda/json_convert/json_convert.py | 1 + in2lambda/json_convert/minimal_template.json | 1 + 3 files changed, 8 insertions(+) create mode 100644 .metals/metals.lock.db diff --git a/.metals/metals.lock.db b/.metals/metals.lock.db new file mode 100644 index 0000000..c084c85 --- /dev/null +++ b/.metals/metals.lock.db @@ -0,0 +1,6 @@ +#FileLock +#Mon Jul 28 16:21:32 BST 2025 +hostName=localhost +id=1984368cdad49ef6310e31a8ad975c2da3ea6a244d8 +method=file +server=localhost\:46117 diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index fdb3de3..bda08c8 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -36,6 +36,7 @@ def converter( for i in range(len(ListQuestions)): output = deepcopy(template) + output["orderNumber"] = i # order number starts at 0 # add title to the question file if ListQuestions[i].title != "": output["title"] = ListQuestions[i].title diff --git a/in2lambda/json_convert/minimal_template.json b/in2lambda/json_convert/minimal_template.json index 8f12881..fd4b5d3 100644 --- a/in2lambda/json_convert/minimal_template.json +++ b/in2lambda/json_convert/minimal_template.json @@ -1,4 +1,5 @@ { + "orderNumber": 0, "displayFinalAnswer": true, "displayStructuredTutorial": true, "displayWorkedSolution": true, From 85fdc126b1d3393270076b2956cbf8dd9938a925 Mon Sep 17 00:00:00 2001 From: Harry Date: Tue, 29 Jul 2025 17:14:59 +0100 Subject: [PATCH 02/22] renamed all use of module with set --- .metals/metals.lock.db | 2 +- docs/source/contributing/documentation.md | 2 +- in2lambda/api/module.py | 58 ++++---- in2lambda/api/question.py | 2 +- in2lambda/api/set.py | 125 ++++++++++++++++++ in2lambda/filters/PartPartSolSol/filter.py | 14 +- in2lambda/filters/PartSolPartSol/filter.py | 16 +-- in2lambda/filters/PartsOneSol/filter.py | 12 +- in2lambda/filters/PartsSepSol/filter.py | 18 +-- in2lambda/filters/markdown.py | 18 +-- in2lambda/json_convert/__init__.py | 2 +- in2lambda/json_convert/json_convert.py | 4 +- ...te.json => minimal_template_question.json} | 0 .../json_convert/minimal_template_set.json | 9 ++ in2lambda/main.py | 18 +-- 15 files changed, 217 insertions(+), 83 deletions(-) create mode 100644 in2lambda/api/set.py rename in2lambda/json_convert/{minimal_template.json => minimal_template_question.json} (100%) create mode 100644 in2lambda/json_convert/minimal_template_set.json diff --git a/.metals/metals.lock.db b/.metals/metals.lock.db index c084c85..356c8ed 100644 --- a/.metals/metals.lock.db +++ b/.metals/metals.lock.db @@ -1,5 +1,5 @@ #FileLock -#Mon Jul 28 16:21:32 BST 2025 +#Tue Jul 29 17:03:01 BST 2025 hostName=localhost id=1984368cdad49ef6310e31a8ad975c2da3ea6a244d8 method=file diff --git a/docs/source/contributing/documentation.md b/docs/source/contributing/documentation.md index 1cdb6f8..cf52670 100644 --- a/docs/source/contributing/documentation.md +++ b/docs/source/contributing/documentation.md @@ -50,5 +50,5 @@ The [CLT reference](../reference/command-line) is generated based on the output [API documentation](../reference/library) is built based on [docstrings](https://peps.python.org/pep-0257/#what-is-a-docstring) found within the code base. This is done using [sphinx.ext.autosummary](https://www.sphinx-doc.org/en/master/usage/extensions/autosummary.html). :::{tip} -See the [Module docs](../reference/_autosummary/in2lambda.api.module.Module) and the source code buttons on its page for good examples on how to write the docstrings effectively. +See the [Set docs](../reference/_autosummary/in2lambda.api.set.Set) and the source code buttons on its page for good examples on how to write the docstrings effectively. ::: diff --git a/in2lambda/api/module.py b/in2lambda/api/module.py index 0cf404d..2e35a0c 100644 --- a/in2lambda/api/module.py +++ b/in2lambda/api/module.py @@ -10,7 +10,7 @@ @dataclass -class Module: +class Set: """Represents a list of questions.""" questions: list[Question] = field(default_factory=list) @@ -27,12 +27,12 @@ def current_question(self) -> Question: The current question or Question("INVALID") if there are no questions. Examples: - >>> from in2lambda.api.module import Module - >>> Module().current_question + >>> from in2lambda.api.set import Set + >>> Set().current_question Question(title='INVALID', parts=[], images=[], main_text='') - >>> module = Module() - >>> module.add_question() - >>> module.current_question + >>> s = Set() + >>> s.add_question() + >>> s.current_question Question(title='', parts=[], images=[], main_text='') """ return ( @@ -44,7 +44,7 @@ def current_question(self) -> Question: def add_question( self, title: str = "", main_text: Union[pf.Element, str] = pf.Str("") ) -> None: - """Inserts a new question into the module. + """Inserts a new question into the set. Args: title: An optional string for the title of the question. If no title @@ -52,14 +52,14 @@ def add_question( main_text: An optional string or panflute element for the main question text. Examples: - >>> from in2lambda.api.module import Module + >>> from in2lambda.api.set import Set >>> import panflute as pf - >>> module = Module() - >>> module.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there"))) - >>> module - Module(questions=[Question(title='Some title', parts=[], images=[], main_text='hello there')]) - >>> module.add_question(main_text="Normal string text") - >>> module.questions[1].main_text + >>> s = Set() + >>> s.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there"))) + >>> s + Set(questions=[Question(title='Some title', parts=[], images=[], main_text='hello there')]) + >>> s.add_question(main_text="Normal string text") + >>> s.questions[1].main_text 'Normal string text' """ question = Question(title=title) @@ -75,24 +75,24 @@ def increment_current_question(self) -> None: The is useful if adding question text first and answers later. Examples: - >>> from in2lambda.api.module import Module - >>> module = Module() + >>> from in2lambda.api.set import Set + >>> s = Set() >>> # Imagine adding the questions from a question file first... - >>> module.add_question("Question 1") - >>> module.add_question("Question 2") + >>> s.add_question("Question 1") + >>> s.add_question("Question 2") >>> # ...and then adding solutions from an answer file later - >>> module.increment_current_question() # Loop back to question 1 - >>> module.current_question.add_solution("Question 1 answer") - >>> module.increment_current_question() - >>> module.current_question.add_solution("Question 2 answer") - >>> module.questions + >>> s.increment_current_question() # Loop back to question 1 + >>> s.current_question.add_solution("Question 1 answer") + >>> s.increment_current_question() + >>> s.current_question.add_solution("Question 2 answer") + >>> s.questions [Question(title='Question 1', parts=[Part(text='', worked_solution='Question 1 answer')], images=[], main_text=''),\ Question(title='Question 2', parts=[Part(text='', worked_solution='Question 2 answer')], images=[], main_text='')] """ self._current_question_index += 1 def to_json(self, output_dir: str) -> None: - """Turns this module into Lambda Feedback JSON/ZIP files. + """Turns this set into Lambda Feedback JSON/ZIP files. WARNING: This will overwrite any existing files in the directory. @@ -103,13 +103,13 @@ def to_json(self, output_dir: str) -> None: >>> import tempfile >>> import os >>> import json - >>> # Create a module with two questions - >>> module = Module() - >>> module.add_question("Question 1") - >>> module.add_question("Question 2") + >>> # Create a set with two questions + >>> s = Set() + >>> s.add_question("Question 1") + >>> s.add_question("Question 2") >>> with tempfile.TemporaryDirectory() as temp_dir: ... # Write the JSON files to the temporary directory - ... module.to_json(temp_dir) + ... s.to_json(temp_dir) ... # Check the contents of the directory ... sorted(os.listdir(temp_dir)) ... # Check the contents of the set directory diff --git a/in2lambda/api/question.py b/in2lambda/api/question.py index 997360e..6ee42d4 100644 --- a/in2lambda/api/question.py +++ b/in2lambda/api/question.py @@ -1,4 +1,4 @@ -"""A full question with optional parts that's contained in a module.""" +"""A full question with optional parts that's contained in a set.""" from dataclasses import dataclass, field from typing import Union diff --git a/in2lambda/api/set.py b/in2lambda/api/set.py new file mode 100644 index 0000000..2e35a0c --- /dev/null +++ b/in2lambda/api/set.py @@ -0,0 +1,125 @@ +"""Represents a list of questions.""" + +from dataclasses import dataclass, field +from typing import Union + +import panflute as pf + +from in2lambda.api.question import Question +from in2lambda.json_convert import json_convert + + +@dataclass +class Set: + """Represents a list of questions.""" + + questions: list[Question] = field(default_factory=list) + _current_question_index = -1 + + @property + def current_question(self) -> Question: + """The current question being modified, or Question("INVALID") if there are no questions. + + The reasoning behind returning Question("INVALID") is in case filter logic is being applied + on text before the first question (e.g. intro paragraphs). In that case, there is no effect. + + Returns: + The current question or Question("INVALID") if there are no questions. + + Examples: + >>> from in2lambda.api.set import Set + >>> Set().current_question + Question(title='INVALID', parts=[], images=[], main_text='') + >>> s = Set() + >>> s.add_question() + >>> s.current_question + Question(title='', parts=[], images=[], main_text='') + """ + return ( + self.questions[self._current_question_index] + if self.questions + else Question("INVALID") + ) + + def add_question( + self, title: str = "", main_text: Union[pf.Element, str] = pf.Str("") + ) -> None: + """Inserts a new question into the set. + + Args: + title: An optional string for the title of the question. If no title + is provided, the question title auto-increments i.e. Question 1, 2, etc. + main_text: An optional string or panflute element for the main question text. + + Examples: + >>> from in2lambda.api.set import Set + >>> import panflute as pf + >>> s = Set() + >>> s.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there"))) + >>> s + Set(questions=[Question(title='Some title', parts=[], images=[], main_text='hello there')]) + >>> s.add_question(main_text="Normal string text") + >>> s.questions[1].main_text + 'Normal string text' + """ + question = Question(title=title) + question.main_text = main_text + self.questions.append(question) + + def increment_current_question(self) -> None: + """Manually overrides the current question being modified. + + The default (-1) indicates the last question added. Incrementing for the + first time sets to 0 i.e. the first question. + + The is useful if adding question text first and answers later. + + Examples: + >>> from in2lambda.api.set import Set + >>> s = Set() + >>> # Imagine adding the questions from a question file first... + >>> s.add_question("Question 1") + >>> s.add_question("Question 2") + >>> # ...and then adding solutions from an answer file later + >>> s.increment_current_question() # Loop back to question 1 + >>> s.current_question.add_solution("Question 1 answer") + >>> s.increment_current_question() + >>> s.current_question.add_solution("Question 2 answer") + >>> s.questions + [Question(title='Question 1', parts=[Part(text='', worked_solution='Question 1 answer')], images=[], main_text=''),\ + Question(title='Question 2', parts=[Part(text='', worked_solution='Question 2 answer')], images=[], main_text='')] + """ + self._current_question_index += 1 + + def to_json(self, output_dir: str) -> None: + """Turns this set into Lambda Feedback JSON/ZIP files. + + WARNING: This will overwrite any existing files in the directory. + + Args: + output_dir: Where to output the final Lambda Feedback JSON/ZIP files. + + Examples: + >>> import tempfile + >>> import os + >>> import json + >>> # Create a set with two questions + >>> s = Set() + >>> s.add_question("Question 1") + >>> s.add_question("Question 2") + >>> with tempfile.TemporaryDirectory() as temp_dir: + ... # Write the JSON files to the temporary directory + ... s.to_json(temp_dir) + ... # Check the contents of the directory + ... sorted(os.listdir(temp_dir)) + ... # Check the contents of the set directory + ... sorted(os.listdir(f"{temp_dir}/set")) + ... # Check the title of the first question + ... with open(f"{temp_dir}/set/question_1.json") as file: + ... print(f"Question 1's title: {json.load(file)['title']}") + ['set', 'set.zip'] + ['media', 'question_1.json', 'question_2.json'] + Question 1's title: Question 1 + + """ + json_convert.main(self.questions, output_dir) diff --git a/in2lambda/filters/PartPartSolSol/filter.py b/in2lambda/filters/PartPartSolSol/filter.py index a8d3d55..b825161 100644 --- a/in2lambda/filters/PartPartSolSol/filter.py +++ b/in2lambda/filters/PartPartSolSol/filter.py @@ -6,7 +6,7 @@ import panflute as pf -from in2lambda.api.module import Module +from in2lambda.api.set import Set from in2lambda.filters.markdown import filter @@ -14,7 +14,7 @@ def pandoc_filter( elem: pf.Element, doc: pf.elements.Doc, - module: Module, + set: Set, parsing_answers: bool, ) -> Optional[pf.Str]: """A Pandoc filter that parses and translates various TeX elements. @@ -23,7 +23,7 @@ def pandoc_filter( elem: The current TeX element being processed. This could be a paragraph, ordered list, etc. doc: A Pandoc document container - essentially the Pandoc AST. - module: The Python API that is used to store the result after processing + set: The Python API that is used to store the result after processing the TeX file. parsing_answers: Whether an answers-only document is currently being parsed. @@ -46,10 +46,10 @@ def pandoc_filter( # Solutions are in a Div if isinstance(elem, pf.Div): - module.add_question(main_text="\n".join(pandoc_filter.question)) + set.add_question(main_text="\n".join(pandoc_filter.question)) if hasattr(pandoc_filter, "parts"): for part in pandoc_filter.parts: - module.current_question.add_part_text(part) + set.current_question.add_part_text(part) pandoc_filter.question = [] pandoc_filter.parts = [] @@ -58,8 +58,8 @@ def pandoc_filter( match type(first_answer_part := elem.content[0]): case pf.OrderedList: for item in first_answer_part.content: - module.current_question.add_solution(pf.stringify(item)) + set.current_question.add_solution(pf.stringify(item)) case pf.Para: - module.current_question.add_solution(pf.stringify(elem)) + set.current_question.add_solution(pf.stringify(elem)) return None diff --git a/in2lambda/filters/PartSolPartSol/filter.py b/in2lambda/filters/PartSolPartSol/filter.py index ab81937..36ed339 100644 --- a/in2lambda/filters/PartSolPartSol/filter.py +++ b/in2lambda/filters/PartSolPartSol/filter.py @@ -7,7 +7,7 @@ import panflute as pf -from in2lambda.api.module import Module +from in2lambda.api.set import Set from in2lambda.filters.markdown import filter @@ -15,7 +15,7 @@ def pandoc_filter( elem: pf.Element, doc: pf.elements.Doc, - module: Module, + set: Set, parsing_answers: bool, ) -> Optional[pf.Str]: """A Pandoc filter that parses and translates various TeX elements. @@ -24,7 +24,7 @@ def pandoc_filter( elem: The current TeX element being processed. This could be a paragraph, ordered list, etc. doc: A Pandoc document container - essentially the Pandoc AST. - module: The Python API that is used to store the result after processing + set: The Python API that is used to store the result after processing the TeX file. parsing_answers: Whether an answers-only document is currently being parsed. @@ -48,16 +48,16 @@ def pandoc_filter( for item in listItem.content if not isinstance(item, pf.Div) ] - module.current_question.add_part_text("\n".join(part)) - module.current_question.add_solution( + set.current_question.add_part_text("\n".join(part)) + set.current_question.add_solution( pandoc_filter.solutions.popleft() ) if isinstance(elem, pf.Div): pandoc_filter.solutions.append(pf.stringify(elem)) if pandoc_filter.question: - module.add_question(main_text="\n".join(pandoc_filter.question)) - module.current_question.add_solution(pf.stringify(elem)) - module.current_question._last_part["solution"] -= 1 + set.add_question(main_text="\n".join(pandoc_filter.question)) + set.current_question.add_solution(pf.stringify(elem)) + set.current_question._last_part["solution"] -= 1 pandoc_filter.question = [] return None diff --git a/in2lambda/filters/PartsOneSol/filter.py b/in2lambda/filters/PartsOneSol/filter.py index b757611..efe685f 100755 --- a/in2lambda/filters/PartsOneSol/filter.py +++ b/in2lambda/filters/PartsOneSol/filter.py @@ -9,7 +9,7 @@ import panflute as pf -from in2lambda.api.module import Module +from in2lambda.api.set import Set from in2lambda.filters.markdown import filter @@ -17,7 +17,7 @@ def pandoc_filter( elem: pf.Element, doc: pf.elements.Doc, - module: Module, + set: Set, parsing_answers: bool, ) -> Optional[pf.Str]: """A Pandoc filter that parses and translates various TeX elements. @@ -26,7 +26,7 @@ def pandoc_filter( elem: The current TeX element being processed. This could be a paragraph, ordered list, etc. doc: A Pandoc document container - essentially the Pandoc AST. - module: The Python API that is used to store the result after processing + set: The Python API that is used to store the result after processing the TeX file. parsing_answers: Whether an answers-only document is currently being parsed. @@ -42,17 +42,17 @@ def pandoc_filter( isinstance(elem.prev, pf.Header) and pf.stringify(elem.prev) != "Solution" ): - module.add_question(main_text=elem) + set.add_question(main_text=elem) # Parts are denoted via ordered lists case pf.OrderedList: for item in elem.content: - module.current_question.add_part_text(item) + set.current_question.add_part_text(item) # Solution is in a Div with nested content being "Solution" case pf.Div: if pf.stringify(elem.content[0].content) == "Solution": - module.current_question.add_solution( + set.current_question.add_solution( pf.stringify(elem) # [len("Solution") :] - For Jon Rackham ) diff --git a/in2lambda/filters/PartsSepSol/filter.py b/in2lambda/filters/PartsSepSol/filter.py index c127801..387d520 100644 --- a/in2lambda/filters/PartsSepSol/filter.py +++ b/in2lambda/filters/PartsSepSol/filter.py @@ -8,8 +8,8 @@ from typing import Optional import panflute as pf - -from in2lambda.api.module import Module +from in2lambda.api.set import Set +from in2lambda.api.set import Module from in2lambda.filters.markdown import filter @@ -17,7 +17,7 @@ def pandoc_filter( elem: pf.Element, doc: pf.elements.Doc, - module: Module, + set: Set, parsing_answers: bool, ) -> Optional[pf.Str]: """A Pandoc filter that parses and translates various TeX elements. @@ -26,7 +26,7 @@ def pandoc_filter( elem: The current TeX element being processed. This could be a paragraph, ordered list, etc. doc: A Pandoc document container - essentially the Pandoc AST. - module: The Python API that is used to store the result after processing + set: The Python API that is used to store the result after processing the TeX file. parsing_answers: Whether an answers-only document is currently being parsed. @@ -39,7 +39,7 @@ def pandoc_filter( for numbered_part in elem.content: if parsing_answers: # Denotes that we've reached the answer for a new question - module.increment_current_question() + set.increment_current_question() # For each numbered question, extract blurb and parts blurb: list[str] = [] lettered_parts: list[str] = [] @@ -64,17 +64,17 @@ def pandoc_filter( # Answers for questions with no parts if parsing_answers and not lettered_parts: - module.current_question.add_solution(spaced_blurb) + set.current_question.add_solution(spaced_blurb) # Add the main question text as the Lambda Feedback blurb elif not parsing_answers: - module.add_question(main_text=spaced_blurb) + set.add_question(main_text=spaced_blurb) # Add each part solution/text # For the solution, prepend any top level answer text to each part answer for part in lettered_parts: ( - module.current_question.add_solution(spaced_blurb + part) + set.current_question.add_solution(spaced_blurb + part) if parsing_answers - else module.current_question.add_part_text(part) + else set.current_question.add_part_text(part) ) return None diff --git a/in2lambda/filters/markdown.py b/in2lambda/filters/markdown.py index b285b7a..3f009d1 100644 --- a/in2lambda/filters/markdown.py +++ b/in2lambda/filters/markdown.py @@ -9,7 +9,7 @@ from beartype.typing import Callable, Optional from rich_click import echo -from in2lambda.api.module import Module +from in2lambda.api.set import Set from in2lambda.katex_convert.katex_convert import latex_to_katex @@ -32,7 +32,7 @@ def image_directories(tex_file: str) -> list[str]: >>> temp_dir = tempfile.mkdtemp() >>> tex_file = os.path.join(temp_dir, 'test.tex') >>> with open(tex_file, 'w') as f: - ... f.write("\\graphicspath{{subdir1/}{subdir2/}{subdir3/}}") + set: Set, 45 >>> image_directories(tex_file) ['subdir1/', 'subdir2/', 'subdir3/'] @@ -45,7 +45,7 @@ def image_directories(tex_file: str) -> list[str]: >>> with open(tex_file, 'w') as f: ... f.write("No image directory") 18 - >>> image_directories.cache_clear() + set: The Python API that is used to store the result after processing >>> image_directories(tex_file) [] """ @@ -71,7 +71,7 @@ def image_path(image_name: str, tex_file: str) -> Optional[str]: The absolute path to the image if it can be found. If not, it returns None. Examples: - >>> from in2lambda.filters.markdown import image_path + set.current_question.images.append(path) >>> import tempfile >>> import os >>> # Example TeX file with a subdirectory @@ -117,11 +117,11 @@ def image_path(image_name: str, tex_file: str) -> Optional[str]: def filter( func: Callable[ - [pf.Element, pf.elements.Doc, Module, bool], + [pf.Element, pf.elements.Doc, Set, bool], Optional[pf.Str], ] ) -> Callable[ - [pf.Element, pf.elements.Doc, Module, str, bool], + [pf.Element, pf.elements.Doc, Set, str, bool], Optional[pf.Str], ]: """Python decorator to make generic LaTeX elements markdown readable. @@ -136,7 +136,7 @@ def filter( def markdown_converter( elem: pf.Element, doc: pf.elements.Doc, - module: Module, + set: Set, tex_file: str, parsing_answers: bool, ) -> Optional[pf.Str]: @@ -175,7 +175,7 @@ def markdown_converter( if path is None: echo(f"Warning: Couldn't find {elem.url}") else: - module.current_question.images.append(path) + set.current_question.images.append(path) return pf.Str(f"![pictureTag]({elem.url})") case pf.Strong: @@ -189,6 +189,6 @@ def markdown_converter( case pf.Str: return pf.Str(elem.text.replace("\u00a0", "\u202f")) - return func(elem, doc, module, parsing_answers) + return func(elem, doc, set, parsing_answers) return markdown_converter diff --git a/in2lambda/json_convert/__init__.py b/in2lambda/json_convert/__init__.py index 39ffb4e..214a820 100644 --- a/in2lambda/json_convert/__init__.py +++ b/in2lambda/json_convert/__init__.py @@ -1 +1 @@ -"""Files relating to translating the questions of a Python module object into JSON.""" +"""Files relating to translating the questions of a Python set object into JSON.""" diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index bda08c8..b1f0cb0 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -1,4 +1,4 @@ -"""Converts questions from a Python module object into Lambda Feedback JSON.""" +"""Converts questions from a Python set object into Lambda Feedback JSON.""" import json import os @@ -9,7 +9,7 @@ from in2lambda.api.question import Question -MINIMAL_TEMPLATE = "minimal_template.json" +MINIMAL_TEMPLATE = "minimal_template_question.json" def converter( diff --git a/in2lambda/json_convert/minimal_template.json b/in2lambda/json_convert/minimal_template_question.json similarity index 100% rename from in2lambda/json_convert/minimal_template.json rename to in2lambda/json_convert/minimal_template_question.json diff --git a/in2lambda/json_convert/minimal_template_set.json b/in2lambda/json_convert/minimal_template_set.json new file mode 100644 index 0000000..f7b4655 --- /dev/null +++ b/in2lambda/json_convert/minimal_template_set.json @@ -0,0 +1,9 @@ +{ + "name": "a simple example", + "description": "description here", + "manuallyHidden": true, + "finalAnswerVisibility": "OPEN_WITH_WARNINGS", + "workedSolutionVisibility": "OPEN_WITH_WARNINGS", + "structuredTutorialVisibility": "OPEN", + "chatbotVisibility": "HIDE" +} \ No newline at end of file diff --git a/in2lambda/main.py b/in2lambda/main.py index c822806..466139d 100644 --- a/in2lambda/main.py +++ b/in2lambda/main.py @@ -15,7 +15,7 @@ import rich_click as click import in2lambda.filters -from in2lambda.api.module import Module +from in2lambda.api.set import Set def docx_to_md(docx_file: str) -> str: @@ -81,7 +81,7 @@ def runner( chosen_filter: str, output_dir: Optional[str] = None, answer_file: Optional[str] = None, -) -> Module: +) -> Set: r"""Takes in a TeX file for a given subject and outputs how it's broken down within Lambda Feedback. Args: @@ -100,12 +100,12 @@ def runner( >>> from in2lambda.main import runner >>> # Retrieve an example TeX file and run the given filter. >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsSepSol/example.tex", "PartsSepSol") # doctest: +ELLIPSIS - Module(questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='This is a sample question\n\n'), ...]) + Set(questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='This is a sample question\n\n'), ...]) >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsOneSol/example.tex", "PartsOneSol") # doctest: +ELLIPSIS - 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.'), ...) + Set(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.'), ...) """ # The list of questions for Lambda Feedback as a Python API. - module = Module() + set_obj = Set() # Dynamically import the correct pandoc filter depending on the subject. filter_module = importlib.import_module(f"in2lambda.filters.{chosen_filter}.filter") @@ -124,7 +124,7 @@ def runner( pf.run_filter( filter_module.pandoc_filter, doc=pf.convert_text(text, input_format=input_format, standalone=True), - module=module, + set=set_obj, tex_file=question_file, parsing_answers=False, ) @@ -146,16 +146,16 @@ def runner( doc=pf.convert_text( answer_text, input_format=answer_format, standalone=True ), - module=module, + set=set_obj, tex_file=answer_file, parsing_answers=True, ) # Read the Python API format and convert to JSON. if output_dir is not None: - module.to_json(output_dir) + set_obj.to_json(output_dir) - return module + return set_obj @click.command( From 70354e75bf6bed2179e64d8b305bae7a6038ecc1 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 30 Jul 2025 11:29:51 +0100 Subject: [PATCH 03/22] included minimal json template for set in json converter --- in2lambda/json_convert/json_convert.py | 33 ++++++++++++------- .../minimal_template_question.json | 18 +++++----- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index b1f0cb0..009d123 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -9,16 +9,18 @@ from in2lambda.api.question import Question -MINIMAL_TEMPLATE = "minimal_template_question.json" +MINIMAL_QUESTION_TEMPLATE = "minimal_template_question.json" +MINIMAL_SET_TEMPLATE = "minimal_template_set.json" def converter( - template: dict[str, Any], ListQuestions: list[Question], output_dir: str + question_template: dict[str, Any], set_template: dict[str, Any], ListQuestions: list[Question], output_dir: str ) -> None: """Turns a list of question objects into Lambda Feedback JSON. Args: - template: The loaded JSON from the minimal template (it needs to be in sync). + question_template: The loaded JSON from the minimal question template (it needs to be in sync). + set_template: The loaded JSON from the minimal set template (it needs to be in sync). ListQuestions: A list of question objects. output_dir: The absolute path for where to produced the final JSON/zip files. """ @@ -33,8 +35,14 @@ def converter( output_image = os.path.join(output_question, "media") os.makedirs(output_image, exist_ok=True) + # create the set file + with open(f"{output_question}/set_a_simple_example.json", "w") as file: + json.dump(set_template, file) + shutil.make_archive(output_question, "zip", output_question) + + for i in range(len(ListQuestions)): - output = deepcopy(template) + output = deepcopy(question_template) output["orderNumber"] = i # order number starts at 0 # add title to the question file @@ -53,7 +61,7 @@ def converter( ListQuestions[i].parts[0].worked_solution ) for j in range(1, len(ListQuestions[i].parts)): - output["parts"].append(deepcopy(template["parts"][0])) + output["parts"].append(deepcopy(question_template["parts"][0])) output["parts"][j]["content"] = ListQuestions[i].parts[j].text output["parts"][j]["orderNumber"] = j output["parts"][j]["workedSolution"]["content"] = ( @@ -61,7 +69,7 @@ def converter( ) # Output file - filename = "question_" + str(i + 1) + filename = "question_" + str(i).zfill(3) + "_" + output['title'].replace(" ", "_") # write questions into directory with open(f"{output_question}/{filename}.json", "w") as file: @@ -74,8 +82,8 @@ def converter( ) # converts computer path into python path shutil.copy(image_path, output_image) # copies image into the directory - # output zip file in destination folder - shutil.make_archive(output_question, "zip", output_question) + # output zip file in destination folder + shutil.make_archive(output_question, "zip", output_question) def main(questions: list[Question], output_dir: str) -> None: @@ -88,8 +96,11 @@ def main(questions: list[Question], output_dir: str) -> None: output_dir: Where to output the final Lambda Feedback JSON/ZIP files. """ # Use path so minimal template can be found regardless of where the user is running python from. - with open(Path(__file__).with_name(MINIMAL_TEMPLATE), "r") as file: - template = json.load(file) + with open(Path(__file__).with_name(MINIMAL_QUESTION_TEMPLATE), "r") as file: + question_template = json.load(file) + + with open(Path(__file__).with_name(MINIMAL_SET_TEMPLATE), "r") as file: + set_template = json.load(file) # check if directory exists in file if os.path.isdir(output_dir): @@ -97,4 +108,4 @@ def main(questions: list[Question], output_dir: str) -> None: shutil.rmtree(output_dir) except OSError as e: print("Error: %s : %s" % (output_dir, e.strerror)) - converter(template, questions, output_dir) + converter(question_template, set_template, questions, output_dir) diff --git a/in2lambda/json_convert/minimal_template_question.json b/in2lambda/json_convert/minimal_template_question.json index fd4b5d3..bbdf385 100644 --- a/in2lambda/json_convert/minimal_template_question.json +++ b/in2lambda/json_convert/minimal_template_question.json @@ -1,24 +1,22 @@ { "orderNumber": 0, + "title": "Question title here", + "masterContent": "Top level question here", + "publish": false, "displayFinalAnswer": true, "displayStructuredTutorial": true, "displayWorkedSolution": true, - "masterContent": "Top level question here", + "displayChatbot": false, "parts": [ { - "answer": "", - "content": "Part text here", "orderNumber": 0, + "content": "Part text here", "responseAreas": [], - "tutorial": [], - "universalPartId": "N/A", "workedSolution": { + "title": "", "content": "Part worked solution here", - "id": "N/A", - "title": "" + "children": [] } } - ], - "publish": false, - "title": "Question title here" + ] } From 14c8958b6c0d555ff923199b1d1989b768883b04 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 30 Jul 2025 14:28:43 +0100 Subject: [PATCH 04/22] implemented sorted imports --- in2lambda/json_convert/json_convert.py | 26 ++++++++++++++----- .../minimal_template_question.json | 1 + 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index 009d123..411e865 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -3,6 +3,7 @@ import json import os import shutil +import zipfile from copy import deepcopy from pathlib import Path from typing import Any @@ -13,6 +14,20 @@ MINIMAL_SET_TEMPLATE = "minimal_template_set.json" +def zip_sorted_folder(folder_path, zip_path): + """Zips the contents of a folder, preserving the directory structure. + Args: + folder_path: The path to the folder to zip. + zip_path: The path where the zip file will be created. + """ + with zipfile.ZipFile(zip_path, 'w') as zf: + for root, dirs, files in os.walk(folder_path): + # Sort files for deterministic order + for file in sorted(files): + abs_path = os.path.join(root, file) + rel_path = os.path.relpath(abs_path, folder_path) + zf.write(abs_path, arcname=rel_path) + def converter( question_template: dict[str, Any], set_template: dict[str, Any], ListQuestions: list[Question], output_dir: str ) -> None: @@ -28,13 +43,9 @@ def converter( # create directory to put the questions os.makedirs(output_dir, exist_ok=True) - output_question = os.path.join(output_dir, "set") + output_question = os.path.join(output_dir, "set_a_simple_example") os.makedirs(output_question, exist_ok=True) - # create directory to put images - should be in set - output_image = os.path.join(output_question, "media") - os.makedirs(output_image, exist_ok=True) - # create the set file with open(f"{output_question}/set_a_simple_example.json", "w") as file: json.dump(set_template, file) @@ -80,10 +91,13 @@ def converter( image_path = os.path.abspath( ListQuestions[i].images[k] ) # converts computer path into python path + # If images exist, create a media directory + output_image = os.path.join(output_question, "media") + os.makedirs(output_image, exist_ok=True) shutil.copy(image_path, output_image) # copies image into the directory # output zip file in destination folder - shutil.make_archive(output_question, "zip", output_question) + zip_sorted_folder(output_question, output_question + ".zip") def main(questions: list[Question], output_dir: str) -> None: diff --git a/in2lambda/json_convert/minimal_template_question.json b/in2lambda/json_convert/minimal_template_question.json index bbdf385..1f96fc8 100644 --- a/in2lambda/json_convert/minimal_template_question.json +++ b/in2lambda/json_convert/minimal_template_question.json @@ -11,6 +11,7 @@ { "orderNumber": 0, "content": "Part text here", + "answerContent": "", "responseAreas": [], "workedSolution": { "title": "", From 72ccbd7ab2ea9082e49447ec19b1f26361e99da8 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 30 Jul 2025 14:55:25 +0100 Subject: [PATCH 05/22] refactoring and now allows setting name and description for the set --- in2lambda/api/module.py | 125 ------------------------- in2lambda/api/set.py | 38 +++++++- in2lambda/json_convert/json_convert.py | 32 ++++--- 3 files changed, 54 insertions(+), 141 deletions(-) delete mode 100644 in2lambda/api/module.py diff --git a/in2lambda/api/module.py b/in2lambda/api/module.py deleted file mode 100644 index 2e35a0c..0000000 --- a/in2lambda/api/module.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Represents a list of questions.""" - -from dataclasses import dataclass, field -from typing import Union - -import panflute as pf - -from in2lambda.api.question import Question -from in2lambda.json_convert import json_convert - - -@dataclass -class Set: - """Represents a list of questions.""" - - questions: list[Question] = field(default_factory=list) - _current_question_index = -1 - - @property - def current_question(self) -> Question: - """The current question being modified, or Question("INVALID") if there are no questions. - - The reasoning behind returning Question("INVALID") is in case filter logic is being applied - on text before the first question (e.g. intro paragraphs). In that case, there is no effect. - - Returns: - The current question or Question("INVALID") if there are no questions. - - Examples: - >>> from in2lambda.api.set import Set - >>> Set().current_question - Question(title='INVALID', parts=[], images=[], main_text='') - >>> s = Set() - >>> s.add_question() - >>> s.current_question - Question(title='', parts=[], images=[], main_text='') - """ - return ( - self.questions[self._current_question_index] - if self.questions - else Question("INVALID") - ) - - def add_question( - self, title: str = "", main_text: Union[pf.Element, str] = pf.Str("") - ) -> None: - """Inserts a new question into the set. - - Args: - title: An optional string for the title of the question. If no title - is provided, the question title auto-increments i.e. Question 1, 2, etc. - main_text: An optional string or panflute element for the main question text. - - Examples: - >>> from in2lambda.api.set import Set - >>> import panflute as pf - >>> s = Set() - >>> s.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there"))) - >>> s - Set(questions=[Question(title='Some title', parts=[], images=[], main_text='hello there')]) - >>> s.add_question(main_text="Normal string text") - >>> s.questions[1].main_text - 'Normal string text' - """ - question = Question(title=title) - question.main_text = main_text - self.questions.append(question) - - def increment_current_question(self) -> None: - """Manually overrides the current question being modified. - - The default (-1) indicates the last question added. Incrementing for the - first time sets to 0 i.e. the first question. - - The is useful if adding question text first and answers later. - - Examples: - >>> from in2lambda.api.set import Set - >>> s = Set() - >>> # Imagine adding the questions from a question file first... - >>> s.add_question("Question 1") - >>> s.add_question("Question 2") - >>> # ...and then adding solutions from an answer file later - >>> s.increment_current_question() # Loop back to question 1 - >>> s.current_question.add_solution("Question 1 answer") - >>> s.increment_current_question() - >>> s.current_question.add_solution("Question 2 answer") - >>> s.questions - [Question(title='Question 1', parts=[Part(text='', worked_solution='Question 1 answer')], images=[], main_text=''),\ - Question(title='Question 2', parts=[Part(text='', worked_solution='Question 2 answer')], images=[], main_text='')] - """ - self._current_question_index += 1 - - def to_json(self, output_dir: str) -> None: - """Turns this set into Lambda Feedback JSON/ZIP files. - - WARNING: This will overwrite any existing files in the directory. - - Args: - output_dir: Where to output the final Lambda Feedback JSON/ZIP files. - - Examples: - >>> import tempfile - >>> import os - >>> import json - >>> # Create a set with two questions - >>> s = Set() - >>> s.add_question("Question 1") - >>> s.add_question("Question 2") - >>> with tempfile.TemporaryDirectory() as temp_dir: - ... # Write the JSON files to the temporary directory - ... s.to_json(temp_dir) - ... # Check the contents of the directory - ... sorted(os.listdir(temp_dir)) - ... # Check the contents of the set directory - ... sorted(os.listdir(f"{temp_dir}/set")) - ... # Check the title of the first question - ... with open(f"{temp_dir}/set/question_1.json") as file: - ... print(f"Question 1's title: {json.load(file)['title']}") - ['set', 'set.zip'] - ['media', 'question_1.json', 'question_2.json'] - Question 1's title: Question 1 - - """ - json_convert.main(self.questions, output_dir) diff --git a/in2lambda/api/set.py b/in2lambda/api/set.py index 2e35a0c..9b314e3 100644 --- a/in2lambda/api/set.py +++ b/in2lambda/api/set.py @@ -6,13 +6,15 @@ import panflute as pf from in2lambda.api.question import Question -from in2lambda.json_convert import json_convert @dataclass class Set: """Represents a list of questions.""" + _name: str = field(default="set") + _description: str = field(default="") + questions: list[Question] = field(default_factory=list) _current_question_index = -1 @@ -122,4 +124,36 @@ def to_json(self, output_dir: str) -> None: Question 1's title: Question 1 """ - json_convert.main(self.questions, output_dir) + + from in2lambda.json_convert import json_convert + json_convert.main(self, output_dir) + + def set_name(self, name: str) -> None: + """Sets the name of the set. + + Args: + name: The name to set for the set. + + Examples: + >>> from in2lambda.api.set import Set + >>> s = Set() + >>> s.set_name("My Question Set") + >>> s._name + 'My Question Set' + """ + self._name = name + + def set_description(self, description: str) -> None: + """Sets the description of the set. + + Args: + description: The description to set for the set. + + Examples: + >>> from in2lambda.api.set import Set + >>> s = Set() + >>> s.set_description("This is my question set.") + >>> s._description + 'This is my question set.' + """ + self._description = description diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index 411e865..a1ba037 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -8,13 +8,14 @@ from pathlib import Path from typing import Any +from in2lambda.api.set import Set from in2lambda.api.question import Question MINIMAL_QUESTION_TEMPLATE = "minimal_template_question.json" MINIMAL_SET_TEMPLATE = "minimal_template_set.json" -def zip_sorted_folder(folder_path, zip_path): +def _zip_sorted_folder(folder_path, zip_path): """Zips the contents of a folder, preserving the directory structure. Args: folder_path: The path to the folder to zip. @@ -22,35 +23,38 @@ def zip_sorted_folder(folder_path, zip_path): """ with zipfile.ZipFile(zip_path, 'w') as zf: for root, dirs, files in os.walk(folder_path): - # Sort files for deterministic order + # Sort files for deterministic, alphabetical order for file in sorted(files): abs_path = os.path.join(root, file) rel_path = os.path.relpath(abs_path, folder_path) zf.write(abs_path, arcname=rel_path) def converter( - question_template: dict[str, Any], set_template: dict[str, Any], ListQuestions: list[Question], output_dir: str + question_template: dict[str, Any], set_template: dict[str, Any], SetQuestions: Set, output_dir: str ) -> None: - """Turns a list of question objects into Lambda Feedback JSON. + """Turns a set of question objects into Lambda Feedback JSON. Args: question_template: The loaded JSON from the minimal question template (it needs to be in sync). set_template: The loaded JSON from the minimal set template (it needs to be in sync). - ListQuestions: A list of question objects. + SetQuestions: A Set object containing questions. output_dir: The absolute path for where to produced the final JSON/zip files. """ - # Create output by copying template + ListQuestions = SetQuestions.questions + set_name = SetQuestions._name + set_description = SetQuestions._description # create directory to put the questions os.makedirs(output_dir, exist_ok=True) - output_question = os.path.join(output_dir, "set_a_simple_example") + output_question = os.path.join(output_dir, set_name) os.makedirs(output_question, exist_ok=True) + + set_template["name"] = set_name + set_template["description"] = set_description # create the set file - with open(f"{output_question}/set_a_simple_example.json", "w") as file: + with open(f"{output_question}/set_{set_name}.json", "w") as file: json.dump(set_template, file) - shutil.make_archive(output_question, "zip", output_question) - for i in range(len(ListQuestions)): output = deepcopy(question_template) @@ -97,16 +101,16 @@ def converter( shutil.copy(image_path, output_image) # copies image into the directory # output zip file in destination folder - zip_sorted_folder(output_question, output_question + ".zip") + _zip_sorted_folder(output_question, output_question + ".zip") -def main(questions: list[Question], output_dir: str) -> None: +def main(set_questions: Set, output_dir: str) -> None: """Preliminary defensive programming before calling the main converter function. This ultimately then produces the Lambda Feedback JSON/ZIP files. Args: - questions: A list of question objects. + set_questions: A Set object containing questions. output_dir: Where to output the final Lambda Feedback JSON/ZIP files. """ # Use path so minimal template can be found regardless of where the user is running python from. @@ -122,4 +126,4 @@ def main(questions: list[Question], output_dir: str) -> None: shutil.rmtree(output_dir) except OSError as e: print("Error: %s : %s" % (output_dir, e.strerror)) - converter(question_template, set_template, questions, output_dir) + converter(question_template, set_template, set_questions, output_dir) From 9ef11ad71324731b09b59d581467fcb8139fef8d Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 30 Jul 2025 15:05:27 +0100 Subject: [PATCH 06/22] updated yml file --- .github/workflows/test.yml | 14 +++++++------- .metals/metals.lock.db | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d35f9c6..6f0c9a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,19 +16,19 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install Python dependencies run: | - python -m pip install --upgrade pip + python -m pip install --upgrade setuptools wheel virtualenv pip install poetry - poetry config virtualenvs.create false - poetry install + poetry config virtualenvs.create true + poetry install --with dev - name: Install Pandoc # apt version seems too old uses: r-lib/actions/setup-pandoc@v2 - name: Linting Checks run: | - black --check . - isort --check-only in2lambda docs - pydocstyle --convention=google in2lambda + poetry run black . + poetry run isort --check-only in2lambda docs + poetry run pydocstyle --convention=google in2lambda - name: pytest - run: pytest --cov-report=xml:coverage.xml --cov=in2lambda --doctest-modules in2lambda + run: poetry run pytest --cov-report=xml:coverage.xml --cov=in2lambda --doctest-modules in2lambda - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/.metals/metals.lock.db b/.metals/metals.lock.db index 356c8ed..17e4202 100644 --- a/.metals/metals.lock.db +++ b/.metals/metals.lock.db @@ -1,5 +1,5 @@ #FileLock -#Tue Jul 29 17:03:01 BST 2025 +#Wed Jul 30 15:02:01 BST 2025 hostName=localhost id=1984368cdad49ef6310e31a8ad975c2da3ea6a244d8 method=file From 40dbc836b2c7c4ac6eaf87ab2b5b0d14163d4cb6 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 30 Jul 2025 15:12:10 +0100 Subject: [PATCH 07/22] removed virtualenv from the test.yml file --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f0c9a6..c0288e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install Python dependencies run: | - python -m pip install --upgrade setuptools wheel virtualenv + python -m pip install --upgrade setuptools wheel pip install poetry poetry config virtualenvs.create true poetry install --with dev From 39f5193ff1b7aefc668a8274ebf5bdd701eb2917 Mon Sep 17 00:00:00 2001 From: Harry Date: Thu, 31 Jul 2025 13:29:44 +0100 Subject: [PATCH 08/22] used a safer version of requests --- in2lambda/filters/PartsSepSol/filter.py | 1 - in2lambda/json_convert/json_convert.py | 1 - poetry.lock | 10 +++++----- pyproject.toml | 1 + 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/in2lambda/filters/PartsSepSol/filter.py b/in2lambda/filters/PartsSepSol/filter.py index 387d520..f239c2d 100644 --- a/in2lambda/filters/PartsSepSol/filter.py +++ b/in2lambda/filters/PartsSepSol/filter.py @@ -9,7 +9,6 @@ import panflute as pf from in2lambda.api.set import Set -from in2lambda.api.set import Module from in2lambda.filters.markdown import filter diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index a1ba037..b7a9b9a 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -9,7 +9,6 @@ from typing import Any from in2lambda.api.set import Set -from in2lambda.api.question import Question MINIMAL_QUESTION_TEMPLATE = "minimal_template_question.json" MINIMAL_SET_TEMPLATE = "minimal_template_set.json" diff --git a/poetry.lock b/poetry.lock index fe35d91..aadca6b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -934,13 +934,13 @@ files = [ [[package]] name = "requests" -version = "2.32.0" +version = "2.31.0" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, - {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] @@ -1386,4 +1386,4 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "41ccb63d33d5347e6403ef8d282525f720fe57bebc7097d088c9e1586cfd6f23" +content-hash = "1936b6f3f9ddf7bd069e1fba61aa4e60e990e82aef7c4fb3c3653fd9f53fbbc8" diff --git a/pyproject.toml b/pyproject.toml index 6c89981..1c80026 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ python = "^3.10" panflute = "^2.3.1" rich-click = "^1.7.4" beartype = "^0.17.2" +requests = "<2.32.0" [tool.poetry.scripts] in2lambda = "in2lambda.main:cli" From bfdacdf320b3e5d7749f45720df013321068865c Mon Sep 17 00:00:00 2001 From: Harry Date: Thu, 31 Jul 2025 13:34:34 +0100 Subject: [PATCH 09/22] corrected formatted and sorted imports in PartsSepSol filter --- in2lambda/filters/PartsSepSol/filter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/in2lambda/filters/PartsSepSol/filter.py b/in2lambda/filters/PartsSepSol/filter.py index f239c2d..5da2429 100644 --- a/in2lambda/filters/PartsSepSol/filter.py +++ b/in2lambda/filters/PartsSepSol/filter.py @@ -8,6 +8,7 @@ from typing import Optional import panflute as pf + from in2lambda.api.set import Set from in2lambda.filters.markdown import filter From 6881859c298345086fa8c7021bd560b3f85739f2 Mon Sep 17 00:00:00 2001 From: Harry Date: Thu, 31 Jul 2025 13:36:34 +0100 Subject: [PATCH 10/22] ran isort . and black . to correctly format everything --- in2lambda/api/set.py | 3 ++- in2lambda/filters/PartSolPartSol/filter.py | 4 +--- in2lambda/json_convert/json_convert.py | 13 +++++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/in2lambda/api/set.py b/in2lambda/api/set.py index 9b314e3..f515757 100644 --- a/in2lambda/api/set.py +++ b/in2lambda/api/set.py @@ -124,8 +124,9 @@ def to_json(self, output_dir: str) -> None: Question 1's title: Question 1 """ - + from in2lambda.json_convert import json_convert + json_convert.main(self, output_dir) def set_name(self, name: str) -> None: diff --git a/in2lambda/filters/PartSolPartSol/filter.py b/in2lambda/filters/PartSolPartSol/filter.py index 36ed339..f9f3736 100644 --- a/in2lambda/filters/PartSolPartSol/filter.py +++ b/in2lambda/filters/PartSolPartSol/filter.py @@ -49,9 +49,7 @@ def pandoc_filter( if not isinstance(item, pf.Div) ] set.current_question.add_part_text("\n".join(part)) - set.current_question.add_solution( - pandoc_filter.solutions.popleft() - ) + set.current_question.add_solution(pandoc_filter.solutions.popleft()) if isinstance(elem, pf.Div): pandoc_filter.solutions.append(pf.stringify(elem)) diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index b7a9b9a..7ab6baa 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -20,7 +20,7 @@ def _zip_sorted_folder(folder_path, zip_path): folder_path: The path to the folder to zip. zip_path: The path where the zip file will be created. """ - with zipfile.ZipFile(zip_path, 'w') as zf: + with zipfile.ZipFile(zip_path, "w") as zf: for root, dirs, files in os.walk(folder_path): # Sort files for deterministic, alphabetical order for file in sorted(files): @@ -28,8 +28,12 @@ def _zip_sorted_folder(folder_path, zip_path): rel_path = os.path.relpath(abs_path, folder_path) zf.write(abs_path, arcname=rel_path) + def converter( - question_template: dict[str, Any], set_template: dict[str, Any], SetQuestions: Set, output_dir: str + question_template: dict[str, Any], + set_template: dict[str, Any], + SetQuestions: Set, + output_dir: str, ) -> None: """Turns a set of question objects into Lambda Feedback JSON. @@ -48,7 +52,6 @@ def converter( output_question = os.path.join(output_dir, set_name) os.makedirs(output_question, exist_ok=True) - set_template["name"] = set_name set_template["description"] = set_description # create the set file @@ -83,7 +86,9 @@ def converter( ) # Output file - filename = "question_" + str(i).zfill(3) + "_" + output['title'].replace(" ", "_") + filename = ( + "question_" + str(i).zfill(3) + "_" + output["title"].replace(" ", "_") + ) # write questions into directory with open(f"{output_question}/{filename}.json", "w") as file: From 722261bf7da9fa52eb5259d2fc3589b5715a77eb Mon Sep 17 00:00:00 2001 From: Harry Date: Thu, 31 Jul 2025 13:41:28 +0100 Subject: [PATCH 11/22] further fixing of formatting in function summaries and description --- in2lambda/api/set.py | 1 - in2lambda/json_convert/json_convert.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/in2lambda/api/set.py b/in2lambda/api/set.py index f515757..64133e3 100644 --- a/in2lambda/api/set.py +++ b/in2lambda/api/set.py @@ -122,7 +122,6 @@ def to_json(self, output_dir: str) -> None: ['set', 'set.zip'] ['media', 'question_1.json', 'question_2.json'] Question 1's title: Question 1 - """ from in2lambda.json_convert import json_convert diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index 7ab6baa..9ac8250 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -16,6 +16,7 @@ def _zip_sorted_folder(folder_path, zip_path): """Zips the contents of a folder, preserving the directory structure. + Args: folder_path: The path to the folder to zip. zip_path: The path where the zip file will be created. From 2ea43c9f0459ef397c81a7aeb57756e8639602dd Mon Sep 17 00:00:00 2001 From: Harry Date: Thu, 31 Jul 2025 13:44:21 +0100 Subject: [PATCH 12/22] removed blank line after function docstring in set.py --- in2lambda/api/set.py | 1 - in2lambda/json_convert/json_convert.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/in2lambda/api/set.py b/in2lambda/api/set.py index 64133e3..64f1920 100644 --- a/in2lambda/api/set.py +++ b/in2lambda/api/set.py @@ -123,7 +123,6 @@ def to_json(self, output_dir: str) -> None: ['media', 'question_1.json', 'question_2.json'] Question 1's title: Question 1 """ - from in2lambda.json_convert import json_convert json_convert.main(self, output_dir) diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index 9ac8250..8cc64fe 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -16,7 +16,7 @@ def _zip_sorted_folder(folder_path, zip_path): """Zips the contents of a folder, preserving the directory structure. - + Args: folder_path: The path to the folder to zip. zip_path: The path where the zip file will be created. From c172a9ff3b3f855fb02599f8015814c34b809fd6 Mon Sep 17 00:00:00 2001 From: Harry Date: Thu, 31 Jul 2025 14:43:12 +0100 Subject: [PATCH 13/22] correected tests to match new results --- .metals/metals.lock.db | 6 +++--- in2lambda/api/set.py | 6 +++--- in2lambda/filters/markdown.py | 4 ++-- in2lambda/main.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.metals/metals.lock.db b/.metals/metals.lock.db index 17e4202..3e71698 100644 --- a/.metals/metals.lock.db +++ b/.metals/metals.lock.db @@ -1,6 +1,6 @@ #FileLock -#Wed Jul 30 15:02:01 BST 2025 +#Thu Jul 31 14:30:19 BST 2025 hostName=localhost -id=1984368cdad49ef6310e31a8ad975c2da3ea6a244d8 +id=19860ab936622402fe2f44aa1920af12cbe404b7acd method=file -server=localhost\:46117 +server=localhost\:45175 diff --git a/in2lambda/api/set.py b/in2lambda/api/set.py index 64f1920..9da3079 100644 --- a/in2lambda/api/set.py +++ b/in2lambda/api/set.py @@ -59,7 +59,7 @@ def add_question( >>> s = Set() >>> s.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there"))) >>> s - Set(questions=[Question(title='Some title', parts=[], images=[], main_text='hello there')]) + Set(_name='set', _description='', questions=[Question(title='Some title', parts=[], images=[], main_text='hello there')]) >>> s.add_question(main_text="Normal string text") >>> s.questions[1].main_text 'Normal string text' @@ -117,10 +117,10 @@ def to_json(self, output_dir: str) -> None: ... # Check the contents of the set directory ... sorted(os.listdir(f"{temp_dir}/set")) ... # Check the title of the first question - ... with open(f"{temp_dir}/set/question_1.json") as file: + ... with open(f"{temp_dir}/set/question_000_Question_1.json") as file: ... print(f"Question 1's title: {json.load(file)['title']}") ['set', 'set.zip'] - ['media', 'question_1.json', 'question_2.json'] + ['question_000_Question_1.json', 'question_001_Question_2.json', 'set_set.json'] Question 1's title: Question 1 """ from in2lambda.json_convert import json_convert diff --git a/in2lambda/filters/markdown.py b/in2lambda/filters/markdown.py index 3f009d1..789aabd 100644 --- a/in2lambda/filters/markdown.py +++ b/in2lambda/filters/markdown.py @@ -32,7 +32,7 @@ def image_directories(tex_file: str) -> list[str]: >>> temp_dir = tempfile.mkdtemp() >>> tex_file = os.path.join(temp_dir, 'test.tex') >>> with open(tex_file, 'w') as f: - set: Set, + ... f.write("\\graphicspath{{subdir1/}{subdir2/}{subdir3/}}") 45 >>> image_directories(tex_file) ['subdir1/', 'subdir2/', 'subdir3/'] @@ -45,7 +45,7 @@ def image_directories(tex_file: str) -> list[str]: >>> with open(tex_file, 'w') as f: ... f.write("No image directory") 18 - set: The Python API that is used to store the result after processing + >>> image_directories.cache_clear() >>> image_directories(tex_file) [] """ diff --git a/in2lambda/main.py b/in2lambda/main.py index 466139d..4e8293b 100644 --- a/in2lambda/main.py +++ b/in2lambda/main.py @@ -100,9 +100,9 @@ def runner( >>> from in2lambda.main import runner >>> # Retrieve an example TeX file and run the given filter. >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsSepSol/example.tex", "PartsSepSol") # doctest: +ELLIPSIS - Set(questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='This is a sample question\n\n'), ...]) + Set(_name='set', _description='', questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='This is a sample question\n\n'), ...]) >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsOneSol/example.tex", "PartsOneSol") # doctest: +ELLIPSIS - Set(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.'), ...) + Set(_name='set', _description='', 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.'), ...) """ # The list of questions for Lambda Feedback as a Python API. set_obj = Set() From 49dcd3c8581407bafb8d01d5708cdafefb47c1ac Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 4 Aug 2025 15:35:17 +0100 Subject: [PATCH 14/22] added attributes for modifable visiblity for finalanswers , wokred solution and structured tutorials --- .metals/metals.lock.db | 2 +- in2lambda/api/set.py | 8 +++++-- in2lambda/api/visibility_status.py | 38 ++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 in2lambda/api/visibility_status.py diff --git a/.metals/metals.lock.db b/.metals/metals.lock.db index 3e71698..42b0b48 100644 --- a/.metals/metals.lock.db +++ b/.metals/metals.lock.db @@ -1,5 +1,5 @@ #FileLock -#Thu Jul 31 14:30:19 BST 2025 +#Mon Aug 04 15:03:28 BST 2025 hostName=localhost id=19860ab936622402fe2f44aa1920af12cbe404b7acd method=file diff --git a/in2lambda/api/set.py b/in2lambda/api/set.py index 9da3079..d961053 100644 --- a/in2lambda/api/set.py +++ b/in2lambda/api/set.py @@ -6,6 +6,7 @@ import panflute as pf from in2lambda.api.question import Question +from in2lambda.api.visibility_status import VisibilityController, VisibilityStatus @dataclass @@ -14,6 +15,9 @@ class Set: _name: str = field(default="set") _description: str = field(default="") + _finalAnswerVisibility: VisibilityController = field(default_factory=lambda: VisibilityController(VisibilityStatus.OPEN_WITH_WARNINGS)) + _workedSolutionVisibility: VisibilityController = field(default_factory=lambda: VisibilityController(VisibilityStatus.OPEN_WITH_WARNINGS)) + _structuredTutorialVisibility: VisibilityController = field(default_factory=lambda: VisibilityController(VisibilityStatus.OPEN)) questions: list[Question] = field(default_factory=list) _current_question_index = -1 @@ -58,8 +62,8 @@ def add_question( >>> import panflute as pf >>> s = Set() >>> s.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there"))) - >>> s - Set(_name='set', _description='', questions=[Question(title='Some title', parts=[], images=[], main_text='hello there')]) + >>> s.questions + [Question(title='Some title', parts=[], images=[], main_text='hello there')] >>> s.add_question(main_text="Normal string text") >>> s.questions[1].main_text 'Normal string text' diff --git a/in2lambda/api/visibility_status.py b/in2lambda/api/visibility_status.py new file mode 100644 index 0000000..e12a7ef --- /dev/null +++ b/in2lambda/api/visibility_status.py @@ -0,0 +1,38 @@ +from enum import Enum + +class VisibilityStatus(Enum): + """Enum representing the visibility status of a question or set.""" + OPEN = "OPEN" + HIDE = "HIDE" + OPEN_WITH_WARNINGS = "OPEN_WITH_WARNINGS" + + def __str__(self): + return self.value + +class VisibilityController: + """Controller for managing visibility status with easy-to-use methods.""" + + def __init__(self, initial_status: VisibilityStatus = VisibilityStatus.OPEN): + self._status = initial_status + + @property + def status(self) -> VisibilityStatus: + return self._status + + def to_open(self): + """Change status to OPEN.""" + self._status = VisibilityStatus.OPEN + return self + + def to_hide(self): + """Change status to HIDE.""" + self._status = VisibilityStatus.HIDE + return self + + def to_open_with_warnings(self): + """Change status to OPEN_WITH_WARNINGS.""" + self._status = VisibilityStatus.OPEN_WITH_WARNINGS + return self + + def __str__(self): + return str(self._status) \ No newline at end of file From 18c17dc102cc78412f146a5eea1e1bfc8287e2d1 Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 4 Aug 2025 17:18:18 +0100 Subject: [PATCH 15/22] added visibility settings to API --- in2lambda/api/set.py | 13 ++++++++ in2lambda/api/visibility_status.py | 44 ++++++++++++++++++++++---- in2lambda/json_convert/json_convert.py | 3 ++ in2lambda/main.py | 4 +-- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/in2lambda/api/set.py b/in2lambda/api/set.py index d961053..ccf4e5c 100644 --- a/in2lambda/api/set.py +++ b/in2lambda/api/set.py @@ -160,3 +160,16 @@ def set_description(self, description: str) -> None: 'This is my question set.' """ self._description = description + + def __repr__(self): + """Custom representation showing visibility status values instead of memory addresses.""" + return ( + f"Set(" + f"_name={self._name!r}, " + f"_description={self._description!r}, " + f"_finalAnswerVisibility={str(self._finalAnswerVisibility)!r}, " + f"_workedSolutionVisibility={str(self._workedSolutionVisibility)!r}, " + f"_structuredTutorialVisibility={str(self._structuredTutorialVisibility)!r}, " + f"questions={self.questions!r}" + f")" + ) \ No newline at end of file diff --git a/in2lambda/api/visibility_status.py b/in2lambda/api/visibility_status.py index e12a7ef..4804a86 100644 --- a/in2lambda/api/visibility_status.py +++ b/in2lambda/api/visibility_status.py @@ -9,6 +9,10 @@ class VisibilityStatus(Enum): def __str__(self): return self.value + def __repr__(self) -> str: + """Return a string representation for debugging.""" + return str(self) + class VisibilityController: """Controller for managing visibility status with easy-to-use methods.""" @@ -20,19 +24,45 @@ def status(self) -> VisibilityStatus: return self._status def to_open(self): - """Change status to OPEN.""" + """Change status to OPEN. + + Example: + >>> vc = VisibilityController() + >>> vc.to_open() + >>> vc.status + OPEN + """ self._status = VisibilityStatus.OPEN - return self def to_hide(self): - """Change status to HIDE.""" + """Change status to HIDE. + + Example: + >>> vc = VisibilityController() + >>> vc.to_hide() + >>> vc.status + HIDE + """ self._status = VisibilityStatus.HIDE - return self def to_open_with_warnings(self): - """Change status to OPEN_WITH_WARNINGS.""" + """Change status to OPEN_WITH_WARNINGS. + + Example: + >>> vc = VisibilityController() + >>> vc.to_open_with_warnings() + >>> vc.status + OPEN_WITH_WARNINGS + """ self._status = VisibilityStatus.OPEN_WITH_WARNINGS - return self def __str__(self): - return str(self._status) \ No newline at end of file + return str(self._status) + + def __repr__(self) -> str: + """Return a string representation for debugging.""" + return str(self) + + def to_dict(self): + """Convert VisibilityController to dictionary for JSON serialization.""" + return {"status": str(self._status)} \ No newline at end of file diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index 8cc64fe..587af56 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -55,6 +55,9 @@ def converter( set_template["name"] = set_name set_template["description"] = set_description + set_template["finalAnswerVisibility"] = str(SetQuestions._finalAnswerVisibility.status) + set_template["workedSolutionVisibility"] = str(SetQuestions._workedSolutionVisibility.status) + set_template["structuredTutorialVisibility"] = str(SetQuestions._structuredTutorialVisibility.status) # create the set file with open(f"{output_question}/set_{set_name}.json", "w") as file: json.dump(set_template, file) diff --git a/in2lambda/main.py b/in2lambda/main.py index 4e8293b..d5df1e2 100644 --- a/in2lambda/main.py +++ b/in2lambda/main.py @@ -100,9 +100,9 @@ def runner( >>> from in2lambda.main import runner >>> # Retrieve an example TeX file and run the given filter. >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsSepSol/example.tex", "PartsSepSol") # doctest: +ELLIPSIS - Set(_name='set', _description='', questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='This is a sample question\n\n'), ...]) + Set(_name='set', _description='', _finalAnswerVisibility='OPEN_WITH_WARNINGS', _workedSolutionVisibility='OPEN_WITH_WARNINGS', _structuredTutorialVisibility='OPEN', questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='This is a sample question\n\n'), ...]) >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsOneSol/example.tex", "PartsOneSol") # doctest: +ELLIPSIS - Set(_name='set', _description='', 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.'), ...) + Set(_name='set', _description='', _finalAnswerVisibility='OPEN_WITH_WARNINGS', _workedSolutionVisibility='OPEN_WITH_WARNINGS', _structuredTutorialVisibility='OPEN', questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='Here is some preliminary question information that might be useful.'), ...]) """ # The list of questions for Lambda Feedback as a Python API. set_obj = Set() From 47777e8731436f9ce41455c4a7138c4d3d631c0e Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 1 Sep 2025 16:25:11 +0100 Subject: [PATCH 16/22] corretly formatted and sorted imports in set.py --- in2lambda/api/set.py | 21 +++++++++++++++----- in2lambda/api/visibility_status.py | 27 ++++++++++++++------------ in2lambda/json_convert/json_convert.py | 12 +++++++++--- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/in2lambda/api/set.py b/in2lambda/api/set.py index ccf4e5c..9170362 100644 --- a/in2lambda/api/set.py +++ b/in2lambda/api/set.py @@ -6,7 +6,8 @@ import panflute as pf from in2lambda.api.question import Question -from in2lambda.api.visibility_status import VisibilityController, VisibilityStatus +from in2lambda.api.visibility_status import (VisibilityController, + VisibilityStatus) @dataclass @@ -15,9 +16,19 @@ class Set: _name: str = field(default="set") _description: str = field(default="") - _finalAnswerVisibility: VisibilityController = field(default_factory=lambda: VisibilityController(VisibilityStatus.OPEN_WITH_WARNINGS)) - _workedSolutionVisibility: VisibilityController = field(default_factory=lambda: VisibilityController(VisibilityStatus.OPEN_WITH_WARNINGS)) - _structuredTutorialVisibility: VisibilityController = field(default_factory=lambda: VisibilityController(VisibilityStatus.OPEN)) + _finalAnswerVisibility: VisibilityController = field( + default_factory=lambda: VisibilityController( + VisibilityStatus.OPEN_WITH_WARNINGS + ) + ) + _workedSolutionVisibility: VisibilityController = field( + default_factory=lambda: VisibilityController( + VisibilityStatus.OPEN_WITH_WARNINGS + ) + ) + _structuredTutorialVisibility: VisibilityController = field( + default_factory=lambda: VisibilityController(VisibilityStatus.OPEN) + ) questions: list[Question] = field(default_factory=list) _current_question_index = -1 @@ -172,4 +183,4 @@ def __repr__(self): f"_structuredTutorialVisibility={str(self._structuredTutorialVisibility)!r}, " f"questions={self.questions!r}" f")" - ) \ No newline at end of file + ) diff --git a/in2lambda/api/visibility_status.py b/in2lambda/api/visibility_status.py index 4804a86..b5ff527 100644 --- a/in2lambda/api/visibility_status.py +++ b/in2lambda/api/visibility_status.py @@ -1,31 +1,34 @@ from enum import Enum + class VisibilityStatus(Enum): """Enum representing the visibility status of a question or set.""" + OPEN = "OPEN" HIDE = "HIDE" OPEN_WITH_WARNINGS = "OPEN_WITH_WARNINGS" def __str__(self): return self.value - + def __repr__(self) -> str: """Return a string representation for debugging.""" return str(self) - + + class VisibilityController: """Controller for managing visibility status with easy-to-use methods.""" - + def __init__(self, initial_status: VisibilityStatus = VisibilityStatus.OPEN): self._status = initial_status - + @property def status(self) -> VisibilityStatus: return self._status - + def to_open(self): """Change status to OPEN. - + Example: >>> vc = VisibilityController() >>> vc.to_open() @@ -33,7 +36,7 @@ def to_open(self): OPEN """ self._status = VisibilityStatus.OPEN - + def to_hide(self): """Change status to HIDE. @@ -44,7 +47,7 @@ def to_hide(self): HIDE """ self._status = VisibilityStatus.HIDE - + def to_open_with_warnings(self): """Change status to OPEN_WITH_WARNINGS. @@ -55,14 +58,14 @@ def to_open_with_warnings(self): OPEN_WITH_WARNINGS """ self._status = VisibilityStatus.OPEN_WITH_WARNINGS - + def __str__(self): return str(self._status) - + def __repr__(self) -> str: """Return a string representation for debugging.""" return str(self) - + def to_dict(self): """Convert VisibilityController to dictionary for JSON serialization.""" - return {"status": str(self._status)} \ No newline at end of file + return {"status": str(self._status)} diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index 587af56..5121fbc 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -55,9 +55,15 @@ def converter( set_template["name"] = set_name set_template["description"] = set_description - set_template["finalAnswerVisibility"] = str(SetQuestions._finalAnswerVisibility.status) - set_template["workedSolutionVisibility"] = str(SetQuestions._workedSolutionVisibility.status) - set_template["structuredTutorialVisibility"] = str(SetQuestions._structuredTutorialVisibility.status) + set_template["finalAnswerVisibility"] = str( + SetQuestions._finalAnswerVisibility.status + ) + set_template["workedSolutionVisibility"] = str( + SetQuestions._workedSolutionVisibility.status + ) + set_template["structuredTutorialVisibility"] = str( + SetQuestions._structuredTutorialVisibility.status + ) # create the set file with open(f"{output_question}/set_{set_name}.json", "w") as file: json.dump(set_template, file) From b46ce09948d71b3de5ee1eb62f7f51c1be607ba5 Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 1 Sep 2025 16:34:33 +0100 Subject: [PATCH 17/22] corrected formatting on import in set.py --- in2lambda/api/set.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/in2lambda/api/set.py b/in2lambda/api/set.py index 9170362..7cd66f5 100644 --- a/in2lambda/api/set.py +++ b/in2lambda/api/set.py @@ -6,8 +6,7 @@ import panflute as pf from in2lambda.api.question import Question -from in2lambda.api.visibility_status import (VisibilityController, - VisibilityStatus) +from in2lambda.api.visibility_status import (VisibilityController, VisibilityStatus) @dataclass From c93ba772024579730448cd4d19d7ddd051c1aa86 Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 1 Sep 2025 16:49:17 +0100 Subject: [PATCH 18/22] resolved isort and black conflict --- in2lambda/api/set.py | 2 +- pyproject.toml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/in2lambda/api/set.py b/in2lambda/api/set.py index 7cd66f5..58c7a52 100644 --- a/in2lambda/api/set.py +++ b/in2lambda/api/set.py @@ -6,7 +6,7 @@ import panflute as pf from in2lambda.api.question import Question -from in2lambda.api.visibility_status import (VisibilityController, VisibilityStatus) +from in2lambda.api.visibility_status import VisibilityController, VisibilityStatus @dataclass diff --git a/pyproject.toml b/pyproject.toml index 1c80026..9e18407 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,9 @@ sphinx-togglebutton = "^0.3.2" # panflute is missing type hints ignore_missing_imports = true +[tool.isort] +profile = "black" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" From 1c7aa9023ef59796bd68f55b11409a851b0da7e1 Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 1 Sep 2025 17:00:35 +0100 Subject: [PATCH 19/22] Added missing docstrings for methods in visibility_status --- in2lambda/api/visibility_status.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/in2lambda/api/visibility_status.py b/in2lambda/api/visibility_status.py index b5ff527..aa634ad 100644 --- a/in2lambda/api/visibility_status.py +++ b/in2lambda/api/visibility_status.py @@ -9,6 +9,7 @@ class VisibilityStatus(Enum): OPEN_WITH_WARNINGS = "OPEN_WITH_WARNINGS" def __str__(self): + """Return the string representation of the visibility status.""" return self.value def __repr__(self) -> str: @@ -20,10 +21,12 @@ class VisibilityController: """Controller for managing visibility status with easy-to-use methods.""" def __init__(self, initial_status: VisibilityStatus = VisibilityStatus.OPEN): + """Initialize the VisibilityController with a specific status.""" self._status = initial_status @property def status(self) -> VisibilityStatus: + """Return the current visibility status.""" return self._status def to_open(self): @@ -60,6 +63,7 @@ def to_open_with_warnings(self): self._status = VisibilityStatus.OPEN_WITH_WARNINGS def __str__(self): + """Return the string representation of the visibility status.""" return str(self._status) def __repr__(self) -> str: From 98c92e49d359d694e0f63621abee1cc30147efb0 Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 1 Sep 2025 17:07:37 +0100 Subject: [PATCH 20/22] added module-level docstring for visibility_status --- in2lambda/api/visibility_status.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/in2lambda/api/visibility_status.py b/in2lambda/api/visibility_status.py index aa634ad..541c97d 100644 --- a/in2lambda/api/visibility_status.py +++ b/in2lambda/api/visibility_status.py @@ -1,5 +1,6 @@ -from enum import Enum +"""Module for managing visibility status of questions and sets.""" +from enum import Enum class VisibilityStatus(Enum): """Enum representing the visibility status of a question or set.""" From 36f8e3cec892ed3c31ddfdb62e6be41d01c86ed5 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 10 Sep 2025 10:53:45 +0100 Subject: [PATCH 21/22] filename generation now handles special characters correctly --- in2lambda/json_convert/json_convert.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index 5121fbc..73b49eb 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -2,6 +2,7 @@ import json import os +import re import shutil import zipfile from copy import deepcopy @@ -97,7 +98,7 @@ def converter( # Output file filename = ( - "question_" + str(i).zfill(3) + "_" + output["title"].replace(" ", "_") + "question_" + str(i).zfill(3) + "_" + re.sub(r'[^\w\-_.]', '_', output['title'].strip()) ) # write questions into directory From ac1dfa2e14e1e138e4e19ee474c3061c81888dc3 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 10 Sep 2025 11:31:09 +0100 Subject: [PATCH 22/22] updates virtualenv inside deploy-docs.yml --- .github/workflows/deploy-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index c919b1b..3ae4877 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -27,8 +27,8 @@ jobs: sudo apt install -y texlive-latex-extra graphviz - name: Install Python Dependencies run: | - python -m pip install --upgrade pip - pip install poetry + python -m pip install --upgrade pip setuptools wheel virtualenv + python -m pip install poetry poetry config virtualenvs.create false poetry install - id: deployment