quizzy 0.1.0__tar.gz → 0.3.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -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