quizzy 0.1.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,15 +5,13 @@ repos:
5
5
  - id: check-yaml
6
6
  - id: end-of-file-fixer
7
7
  - id: trailing-whitespace
8
- - repo: https://github.com/psf/black
9
- rev: "24.10.0"
10
- hooks:
11
- - id: black
12
8
  - repo: https://github.com/charliermarsh/ruff-pre-commit
13
9
  # Ruff version.
14
10
  rev: "v0.8.3"
15
11
  hooks:
16
- - id: ruff
12
+ - id: ruff
13
+ args: [ --fix ]
14
+ - id: ruff-format
17
15
  - repo: https://github.com/pycqa/isort
18
16
  rev: "5.13.2"
19
17
  hooks:
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## v0.3.0
4
+
5
+ * Add *Escape* keybinding to dismiss question and answer modals
6
+ * Add score modifier buttons
7
+
8
+ ## v0.2.0
9
+
10
+ * Added support for Markdown syntax in questions and answers
11
+
12
+ ## v0.1.0
13
+
14
+ * Initial release with basic functionality
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quizzy
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: A Python TUI quiz app
5
5
  Author-email: Jonas Ehrlich <jonas.ehrlich@gmail.com>
6
6
  License-Expression: MIT
@@ -17,7 +17,6 @@ A quiz app using [textual](https://textual.textualize.io/).
17
17
 
18
18
  ![Question board](assets/question-board.png)
19
19
 
20
-
21
20
  ## Configuration and Questions
22
21
 
23
22
  Create a YAML file to define the teams participating, the questions and their answers.
@@ -47,16 +46,21 @@ categories:
47
46
 
48
47
  See [examples/quizzy.yaml](examples/quizzy.yaml) for an example.
49
48
 
50
-
51
49
  ## Running it
52
50
 
53
- Run normally using *uv*:
51
+ Run the latest PyPI release using *uvx*:
52
+
53
+ ```sh
54
+ uvx quizzy projects/gh/quizzy/examples/quizzy.yaml
55
+ ```
56
+
57
+ Run the local version using *uv*:
54
58
 
55
59
  ``` sh
56
60
  uv run quizzy examples/quizzy.yaml
57
61
  ```
58
62
 
59
- Serve using textual:
63
+ Serve on a webserver using textual:
60
64
 
61
65
  ``` sh
62
66
  uv run textual serve "uv run quizzy examples/quizzy.yaml"
@@ -4,7 +4,6 @@ A quiz app using [textual](https://textual.textualize.io/).
4
4
 
5
5
  ![Question board](assets/question-board.png)
6
6
 
7
-
8
7
  ## Configuration and Questions
9
8
 
10
9
  Create a YAML file to define the teams participating, the questions and their answers.
@@ -34,16 +33,21 @@ categories:
34
33
 
35
34
  See [examples/quizzy.yaml](examples/quizzy.yaml) for an example.
36
35
 
37
-
38
36
  ## Running it
39
37
 
40
- Run normally using *uv*:
38
+ Run the latest PyPI release using *uvx*:
39
+
40
+ ```sh
41
+ uvx quizzy projects/gh/quizzy/examples/quizzy.yaml
42
+ ```
43
+
44
+ Run the local version using *uv*:
41
45
 
42
46
  ``` sh
43
47
  uv run quizzy examples/quizzy.yaml
44
48
  ```
45
49
 
46
- Serve using textual:
50
+ Serve on a webserver using textual:
47
51
 
48
52
  ``` sh
49
53
  uv run textual serve "uv run quizzy examples/quizzy.yaml"
@@ -0,0 +1,24 @@
1
+ teams:
2
+ - name: "Team 1"
3
+ - name: "Team 2"
4
+ categories:
5
+ - name: "General Knowledge"
6
+ questions:
7
+ - question: "What is the capital of France?"
8
+ answer: "Paris"
9
+ value: 100
10
+ - question: "What is the capital of Germany?"
11
+ answer: "Berlin"
12
+ value: 200
13
+ - name: "Science"
14
+ questions:
15
+ - question: "What is the _chemical_ **symbol** for [gold](https://duckduckgo.com/?t=ffab&q=gold)?"
16
+ answer: "Au"
17
+ value: 100
18
+ - question: |
19
+ What is the chemical symbol for silver?
20
+
21
+ - We really need to know this one. And don't care about platinum which is Pt.
22
+ - Also not Helium which is He.
23
+ answer: "[Ag](https://en.wikipedia.org/wiki/Gold), that was easy **right**?"
24
+ value: 200
@@ -1,7 +1,7 @@
1
1
 
2
2
  [project]
3
3
  name = "quizzy"
4
- version = "0.1.0"
4
+ version = "0.3.0"
5
5
  description = "A Python TUI quiz app"
6
6
  authors = [{ name = "Jonas Ehrlich", email = "jonas.ehrlich@gmail.com" }]
7
7
  readme = "README.md"
@@ -3,10 +3,13 @@ from __future__ import annotations
3
3
  import argparse
4
4
  import pathlib
5
5
 
6
- from textual import app, containers, log, message, reactive, screen, widgets
6
+ from textual import app, binding, containers, log, message, reactive, screen, widgets
7
7
 
8
8
  from quizzy import __version__, models
9
9
 
10
+ NoCorrectAnswerType = type("NoCorrectAnswerType", (object,), {})
11
+ NoCorrectAnswer = NoCorrectAnswerType()
12
+
10
13
 
11
14
  def get_arg_parser() -> argparse.ArgumentParser:
12
15
  parser = argparse.ArgumentParser(prog=__name__.split(".")[0], description="A terminal quiz app")
@@ -15,9 +18,16 @@ def get_arg_parser() -> argparse.ArgumentParser:
15
18
  return parser
16
19
 
17
20
 
18
- class AnswerScreen(screen.ModalScreen[models.Team | None]):
21
+ QuestionScreenResult = models.Team | NoCorrectAnswerType | None
22
+
23
+
24
+ class AnswerScreen(screen.ModalScreen[QuestionScreenResult], can_focus=True):
19
25
  NOONE_ANSWERED_ID = "__noone-answered"
20
26
 
27
+ BINDINGS = [
28
+ binding.Binding("escape", "no_correct_answer", "Dismiss", key_display="Esc"),
29
+ ]
30
+
21
31
  def __init__(self, category: str, question: models.Question, teams: list[models.Team]) -> None:
22
32
  super().__init__(classes="question-answer-screen")
23
33
  self.category = category
@@ -26,13 +36,13 @@ class AnswerScreen(screen.ModalScreen[models.Team | None]):
26
36
  self.border_title = f"{category} - {question.value} points"
27
37
 
28
38
  def compose(self) -> app.ComposeResult:
29
- question_widget = widgets.Static(self.question.question, id="question")
39
+ question_widget = widgets.Markdown(self.question.question, id="question")
30
40
  question_widget.border_title = "Question"
31
41
 
32
- answer_widget = widgets.Static(self.question.answer, id="answer")
42
+ answer_widget = widgets.Markdown(self.question.answer, id="answer")
33
43
  answer_widget.border_title = "Answer"
34
44
 
35
- whoanswered = containers.HorizontalGroup(
45
+ who_answered = containers.HorizontalGroup(
36
46
  *[
37
47
  containers.Vertical(widgets.Button(team.name, id=team_id, variant="primary"))
38
48
  for team_id, team in self.teams.items()
@@ -40,12 +50,12 @@ class AnswerScreen(screen.ModalScreen[models.Team | None]):
40
50
  id="who-answered",
41
51
  classes="horizontal-100",
42
52
  )
43
- whoanswered.border_title = "Who Answered Correctly?"
53
+ who_answered.border_title = "Who Answered Correctly?"
44
54
 
45
55
  container = containers.Grid(
46
56
  question_widget,
47
57
  answer_widget,
48
- whoanswered,
58
+ who_answered,
49
59
  containers.Horizontal(
50
60
  widgets.Button(
51
61
  "😭 No one answered correctly 😭", id=self.NOONE_ANSWERED_ID, variant="error", classes="button-100"
@@ -57,18 +67,25 @@ class AnswerScreen(screen.ModalScreen[models.Team | None]):
57
67
  )
58
68
 
59
69
  container.border_title = f"{self.category} - {self.question.value} points"
70
+ yield widgets.Footer()
60
71
  yield container
61
72
 
73
+ def action_no_correct_answer(self) -> None:
74
+ self.dismiss(NoCorrectAnswer)
75
+
62
76
  def on_button_pressed(self, event: widgets.Button.Pressed) -> None:
63
77
  if event.button.id == self.NOONE_ANSWERED_ID:
64
- self.dismiss(None)
78
+ self.dismiss(NoCorrectAnswer)
65
79
  elif event.button.id in self.teams:
66
80
  team = self.teams[event.button.id]
67
81
  self.dismiss(team)
68
82
 
69
83
 
70
- class QuestionScreen(screen.ModalScreen[models.Team | None]):
84
+ class QuestionScreen(screen.ModalScreen[QuestionScreenResult], can_focus=True):
71
85
  SHOW_ANSWER_ID = "show-answer"
86
+ BINDINGS = [
87
+ binding.Binding("escape", "dismiss(None)", "Dismiss"),
88
+ ]
72
89
 
73
90
  def __init__(self, category: str, question: models.Question, teams: list[models.Team]) -> None:
74
91
  super().__init__(classes="question-answer-screen")
@@ -77,7 +94,7 @@ class QuestionScreen(screen.ModalScreen[models.Team | None]):
77
94
  self.teams = teams
78
95
 
79
96
  def compose(self) -> app.ComposeResult:
80
- question_widget = widgets.Static(self.question.question, id="question")
97
+ question_widget = widgets.Markdown(self.question.question, id="question")
81
98
  question_widget.border_title = "Question"
82
99
 
83
100
  container = containers.Grid(
@@ -91,17 +108,22 @@ class QuestionScreen(screen.ModalScreen[models.Team | None]):
91
108
  )
92
109
 
93
110
  container.border_title = f"{self.category} - {self.question.value} points"
111
+ yield widgets.Footer()
94
112
  yield container
95
113
 
96
114
  def on_button_pressed(self, event: widgets.Button.Pressed) -> None:
97
- def dismiss(team: models.Team | None) -> None:
115
+ def dismiss(team: QuestionScreenResult) -> None:
98
116
  self.dismiss(team)
99
117
 
100
118
  if event.button.id == self.SHOW_ANSWER_ID:
119
+ event.stop()
101
120
  self.app.push_screen(AnswerScreen(self.category, self.question, self.teams), dismiss)
102
121
 
103
122
 
104
- class TeamScore(containers.Vertical):
123
+ class TeamScore(containers.Horizontal):
124
+ MODIFIER_BUTTON_VALUE = 100
125
+ _ADD_BUTTON_ID = f"add-{MODIFIER_BUTTON_VALUE}"
126
+ _SUBTRACT_BUTTON_ID = f"subtract-{MODIFIER_BUTTON_VALUE}"
105
127
  score = reactive.reactive(0, recompose=True)
106
128
 
107
129
  def __init__(self, team: models.Team) -> None:
@@ -112,6 +134,22 @@ class TeamScore(containers.Vertical):
112
134
 
113
135
  def compose(self) -> app.ComposeResult:
114
136
  yield widgets.Static(str(self.score))
137
+ yield containers.Horizontal(
138
+ widgets.Button("+ 100", id=self._ADD_BUTTON_ID, variant="success"),
139
+ widgets.Button("- 100", id=self._SUBTRACT_BUTTON_ID, variant="error"),
140
+ classes="modifier-buttons-container",
141
+ )
142
+
143
+ def on_button_pressed(self, event: widgets.Button.Pressed) -> None:
144
+ if event.button.id == self._ADD_BUTTON_ID:
145
+ self.score += self.MODIFIER_BUTTON_VALUE
146
+ event.stop()
147
+ elif event.button.id == self._SUBTRACT_BUTTON_ID:
148
+ if self.score <= self.MODIFIER_BUTTON_VALUE:
149
+ self.score = 0
150
+ else:
151
+ self.score -= self.MODIFIER_BUTTON_VALUE
152
+ event.stop()
115
153
 
116
154
  def watch_score(self, score: int) -> None:
117
155
  """
@@ -152,13 +190,14 @@ class QuestionButton(widgets.Button):
152
190
  self.disabled = question.answered
153
191
 
154
192
  def on_click(self) -> None:
155
- # First, disable the button to prevent multiple clicks
156
- self.disabled = True
157
- self.question.answered = True
158
-
159
- def wait_for_result(team: models.Team | None) -> None:
193
+ def wait_for_result(team: QuestionScreenResult) -> None:
160
194
  if team is None:
161
- log("question-button: No-one answered the question")
195
+ return
196
+ # First, disable the button to prevent multiple clicks
197
+ self.disabled = True
198
+ self.question.answered = True
199
+ if isinstance(team, NoCorrectAnswerType):
200
+ log("question-button: No one answered the question")
162
201
  else:
163
202
  log(f"question-button: {team.id} answered the question")
164
203
  self.post_message(self.Answered(team, self.question.value))
@@ -206,3 +245,6 @@ class QuizzyApp(app.App[None]):
206
245
 
207
246
  def on_question_button_answered(self, event: QuestionButton.Answered) -> None:
208
247
  self.scoreboard_widget.update_team_score(event.team.id, event.value)
248
+
249
+ def on_mount(self) -> None:
250
+ self.theme = "textual-light"
@@ -1,8 +1,28 @@
1
1
  TeamScore {
2
- border: solid $primary;
2
+ border: round $primary;
3
3
  align: center top;
4
4
  height: 3;
5
5
  margin: 1;
6
+
7
+ Static {
8
+ max-width: 50%;
9
+ }
10
+
11
+ Horizontal.modifier-buttons-container {
12
+ align: right middle;
13
+
14
+ Button {
15
+ margin: 0 2;
16
+ padding: 0;
17
+ min-width: 8;
18
+ border: none;
19
+ width: 8;
20
+ &:focus {
21
+ /* Disable reversing for text of focussed buttons in the team score row */
22
+ text-style: bold;
23
+ }
24
+ }
25
+ }
6
26
  }
7
27
 
8
28
 
@@ -15,20 +35,20 @@ TeamScore {
15
35
  height: 80%;
16
36
  min-height: 40;
17
37
  padding: 0 2;
18
- border: solid $primary;
38
+ border: round $primary;
19
39
  background: $surface;
20
40
  content-align: center middle;
21
41
 
22
42
  #question {
23
43
  content-align: center middle;
24
44
  padding: 2;
25
- border: solid $foreground 80%;
45
+ border: round $foreground 80%;
26
46
  }
27
47
 
28
48
  #answer {
29
49
  content-align: center middle;
30
50
  padding: 2;
31
- border: solid $success 80%;
51
+ border: round $success 80%;
32
52
 
33
53
  }
34
54
  }
@@ -59,7 +79,7 @@ AnswerScreen {
59
79
  grid-rows: 40% 40% 5 4;
60
80
 
61
81
  #who-answered {
62
- border: solid $foreground 80%;
82
+ border: round $foreground 80%;
63
83
  Button {
64
84
  margin: 0 1;
65
85
  width: 100%;
@@ -840,7 +840,7 @@ wheels = [
840
840
 
841
841
  [[package]]
842
842
  name = "quizzy"
843
- version = "0.1.0"
843
+ version = "0.3.0"
844
844
  source = { editable = "." }
845
845
  dependencies = [
846
846
  { name = "pydantic" },
@@ -1,21 +0,0 @@
1
- teams:
2
- - name: "Team 1"
3
- - name: "Team 2"
4
- categories:
5
- - name: "General Knowledge"
6
- questions:
7
- - question: "What is the capital of France?"
8
- answer: "Paris"
9
- value: 100
10
- - question: "What is the capital of Germany?"
11
- answer: "Berlin"
12
- value: 200
13
- - name: "Science"
14
- questions:
15
- - question: "What is the chemical symbol for gold?"
16
- answer: "Au"
17
- value: 100
18
- - question: "What is the chemical symbol for silver?"
19
- answer: "Ag"
20
- value: 200
21
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes