Skip to content

Commit 9abc753

Browse files
authored
Added ordered question inserting feature and more configuration options for set (#19)
* added ordering to the questions * renamed all use of module with set * included minimal json template for set in json converter * implemented sorted imports * refactoring and now allows setting name and description for the set * updated yml file * removed virtualenv from the test.yml file * used a safer version of requests * corrected formatted and sorted imports in PartsSepSol filter * ran isort . and black . to correctly format everything * further fixing of formatting in function summaries and description * removed blank line after function docstring in set.py * correected tests to match new results * added attributes for modifable visiblity for finalanswers , wokred solution and structured tutorials * added visibility settings to API * corretly formatted and sorted imports in set.py * corrected formatting on import in set.py * resolved isort and black conflict * Added missing docstrings for methods in visibility_status * added module-level docstring for visibility_status * filename generation now handles special characters correctly
1 parent 5f8d7a0 commit 9abc753

File tree

19 files changed

+418
-220
lines changed

19 files changed

+418
-220
lines changed

.github/workflows/test.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,19 @@ jobs:
1616
python-version: ${{ matrix.python-version }}
1717
- name: Install Python dependencies
1818
run: |
19-
python -m pip install --upgrade pip
19+
python -m pip install --upgrade setuptools wheel
2020
pip install poetry
21-
poetry config virtualenvs.create false
22-
poetry install
21+
poetry config virtualenvs.create true
22+
poetry install --with dev
2323
- name: Install Pandoc # apt version seems too old
2424
uses: r-lib/actions/setup-pandoc@v2
2525
- name: Linting Checks
2626
run: |
27-
black --check .
28-
isort --check-only in2lambda docs
29-
pydocstyle --convention=google in2lambda
27+
poetry run black .
28+
poetry run isort --check-only in2lambda docs
29+
poetry run pydocstyle --convention=google in2lambda
3030
- name: pytest
31-
run: pytest --cov-report=xml:coverage.xml --cov=in2lambda --doctest-modules in2lambda
31+
run: poetry run pytest --cov-report=xml:coverage.xml --cov=in2lambda --doctest-modules in2lambda
3232
- name: Upload coverage to Codecov
3333
uses: codecov/codecov-action@v3
3434
with:

.metals/metals.lock.db

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#FileLock
2+
#Mon Aug 04 15:03:28 BST 2025
3+
hostName=localhost
4+
id=19860ab936622402fe2f44aa1920af12cbe404b7acd
5+
method=file
6+
server=localhost\:45175

docs/source/contributing/documentation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@ The [CLT reference](../reference/command-line) is generated based on the output
5050
[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).
5151

5252
:::{tip}
53-
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.
53+
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.
5454
:::

in2lambda/api/module.py

Lines changed: 0 additions & 125 deletions
This file was deleted.

in2lambda/api/question.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""A full question with optional parts that's contained in a module."""
1+
"""A full question with optional parts that's contained in a set."""
22

33
from dataclasses import dataclass, field
44
from typing import Union

in2lambda/api/set.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""Represents a list of questions."""
2+
3+
from dataclasses import dataclass, field
4+
from typing import Union
5+
6+
import panflute as pf
7+
8+
from in2lambda.api.question import Question
9+
from in2lambda.api.visibility_status import VisibilityController, VisibilityStatus
10+
11+
12+
@dataclass
13+
class Set:
14+
"""Represents a list of questions."""
15+
16+
_name: str = field(default="set")
17+
_description: str = field(default="")
18+
_finalAnswerVisibility: VisibilityController = field(
19+
default_factory=lambda: VisibilityController(
20+
VisibilityStatus.OPEN_WITH_WARNINGS
21+
)
22+
)
23+
_workedSolutionVisibility: VisibilityController = field(
24+
default_factory=lambda: VisibilityController(
25+
VisibilityStatus.OPEN_WITH_WARNINGS
26+
)
27+
)
28+
_structuredTutorialVisibility: VisibilityController = field(
29+
default_factory=lambda: VisibilityController(VisibilityStatus.OPEN)
30+
)
31+
32+
questions: list[Question] = field(default_factory=list)
33+
_current_question_index = -1
34+
35+
@property
36+
def current_question(self) -> Question:
37+
"""The current question being modified, or Question("INVALID") if there are no questions.
38+
39+
The reasoning behind returning Question("INVALID") is in case filter logic is being applied
40+
on text before the first question (e.g. intro paragraphs). In that case, there is no effect.
41+
42+
Returns:
43+
The current question or Question("INVALID") if there are no questions.
44+
45+
Examples:
46+
>>> from in2lambda.api.set import Set
47+
>>> Set().current_question
48+
Question(title='INVALID', parts=[], images=[], main_text='')
49+
>>> s = Set()
50+
>>> s.add_question()
51+
>>> s.current_question
52+
Question(title='', parts=[], images=[], main_text='')
53+
"""
54+
return (
55+
self.questions[self._current_question_index]
56+
if self.questions
57+
else Question("INVALID")
58+
)
59+
60+
def add_question(
61+
self, title: str = "", main_text: Union[pf.Element, str] = pf.Str("")
62+
) -> None:
63+
"""Inserts a new question into the set.
64+
65+
Args:
66+
title: An optional string for the title of the question. If no title
67+
is provided, the question title auto-increments i.e. Question 1, 2, etc.
68+
main_text: An optional string or panflute element for the main question text.
69+
70+
Examples:
71+
>>> from in2lambda.api.set import Set
72+
>>> import panflute as pf
73+
>>> s = Set()
74+
>>> s.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there")))
75+
>>> s.questions
76+
[Question(title='Some title', parts=[], images=[], main_text='hello there')]
77+
>>> s.add_question(main_text="Normal string text")
78+
>>> s.questions[1].main_text
79+
'Normal string text'
80+
"""
81+
question = Question(title=title)
82+
question.main_text = main_text
83+
self.questions.append(question)
84+
85+
def increment_current_question(self) -> None:
86+
"""Manually overrides the current question being modified.
87+
88+
The default (-1) indicates the last question added. Incrementing for the
89+
first time sets to 0 i.e. the first question.
90+
91+
The is useful if adding question text first and answers later.
92+
93+
Examples:
94+
>>> from in2lambda.api.set import Set
95+
>>> s = Set()
96+
>>> # Imagine adding the questions from a question file first...
97+
>>> s.add_question("Question 1")
98+
>>> s.add_question("Question 2")
99+
>>> # ...and then adding solutions from an answer file later
100+
>>> s.increment_current_question() # Loop back to question 1
101+
>>> s.current_question.add_solution("Question 1 answer")
102+
>>> s.increment_current_question()
103+
>>> s.current_question.add_solution("Question 2 answer")
104+
>>> s.questions
105+
[Question(title='Question 1', parts=[Part(text='', worked_solution='Question 1 answer')], images=[], main_text=''),\
106+
Question(title='Question 2', parts=[Part(text='', worked_solution='Question 2 answer')], images=[], main_text='')]
107+
"""
108+
self._current_question_index += 1
109+
110+
def to_json(self, output_dir: str) -> None:
111+
"""Turns this set into Lambda Feedback JSON/ZIP files.
112+
113+
WARNING: This will overwrite any existing files in the directory.
114+
115+
Args:
116+
output_dir: Where to output the final Lambda Feedback JSON/ZIP files.
117+
118+
Examples:
119+
>>> import tempfile
120+
>>> import os
121+
>>> import json
122+
>>> # Create a set with two questions
123+
>>> s = Set()
124+
>>> s.add_question("Question 1")
125+
>>> s.add_question("Question 2")
126+
>>> with tempfile.TemporaryDirectory() as temp_dir:
127+
... # Write the JSON files to the temporary directory
128+
... s.to_json(temp_dir)
129+
... # Check the contents of the directory
130+
... sorted(os.listdir(temp_dir))
131+
... # Check the contents of the set directory
132+
... sorted(os.listdir(f"{temp_dir}/set"))
133+
... # Check the title of the first question
134+
... with open(f"{temp_dir}/set/question_000_Question_1.json") as file:
135+
... print(f"Question 1's title: {json.load(file)['title']}")
136+
['set', 'set.zip']
137+
['question_000_Question_1.json', 'question_001_Question_2.json', 'set_set.json']
138+
Question 1's title: Question 1
139+
"""
140+
from in2lambda.json_convert import json_convert
141+
142+
json_convert.main(self, output_dir)
143+
144+
def set_name(self, name: str) -> None:
145+
"""Sets the name of the set.
146+
147+
Args:
148+
name: The name to set for the set.
149+
150+
Examples:
151+
>>> from in2lambda.api.set import Set
152+
>>> s = Set()
153+
>>> s.set_name("My Question Set")
154+
>>> s._name
155+
'My Question Set'
156+
"""
157+
self._name = name
158+
159+
def set_description(self, description: str) -> None:
160+
"""Sets the description of the set.
161+
162+
Args:
163+
description: The description to set for the set.
164+
165+
Examples:
166+
>>> from in2lambda.api.set import Set
167+
>>> s = Set()
168+
>>> s.set_description("This is my question set.")
169+
>>> s._description
170+
'This is my question set.'
171+
"""
172+
self._description = description
173+
174+
def __repr__(self):
175+
"""Custom representation showing visibility status values instead of memory addresses."""
176+
return (
177+
f"Set("
178+
f"_name={self._name!r}, "
179+
f"_description={self._description!r}, "
180+
f"_finalAnswerVisibility={str(self._finalAnswerVisibility)!r}, "
181+
f"_workedSolutionVisibility={str(self._workedSolutionVisibility)!r}, "
182+
f"_structuredTutorialVisibility={str(self._structuredTutorialVisibility)!r}, "
183+
f"questions={self.questions!r}"
184+
f")"
185+
)

0 commit comments

Comments
 (0)