pyjs-examples 0.0.1__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.
Files changed (76) hide show
  1. pyjs_examples-0.0.1/PKG-INFO +71 -0
  2. pyjs_examples-0.0.1/README.md +47 -0
  3. pyjs_examples-0.0.1/__init__.py +1 -0
  4. pyjs_examples-0.0.1/calculator/README.md +55 -0
  5. pyjs_examples-0.0.1/calculator/__init__.py +1 -0
  6. pyjs_examples-0.0.1/calculator/__main__.py +9 -0
  7. pyjs_examples-0.0.1/calculator/calculator.py +325 -0
  8. pyjs_examples-0.0.1/calculator/tests/test_browser_calculator.py +67 -0
  9. pyjs_examples-0.0.1/calculator/tests/test_calculator.py +72 -0
  10. pyjs_examples-0.0.1/catalog.py +1433 -0
  11. pyjs_examples-0.0.1/chatroom/README.md +50 -0
  12. pyjs_examples-0.0.1/chatroom/__init__.py +1 -0
  13. pyjs_examples-0.0.1/chatroom/__main__.py +12 -0
  14. pyjs_examples-0.0.1/chatroom/chat_server.py +156 -0
  15. pyjs_examples-0.0.1/chatroom/chatroom.py +704 -0
  16. pyjs_examples-0.0.1/chatroom/tests/test_browser_chatroom.py +59 -0
  17. pyjs_examples-0.0.1/chatroom/tests/test_chat_server.py +53 -0
  18. pyjs_examples-0.0.1/chatroom/visual_test.py +156 -0
  19. pyjs_examples-0.0.1/counter.py +73 -0
  20. pyjs_examples-0.0.1/kanban/README.md +31 -0
  21. pyjs_examples-0.0.1/kanban/__init__.py +1 -0
  22. pyjs_examples-0.0.1/kanban/__main__.py +8 -0
  23. pyjs_examples-0.0.1/kanban/kanban.py +581 -0
  24. pyjs_examples-0.0.1/kanban/tests/renders/kanban-board.png +0 -0
  25. pyjs_examples-0.0.1/kanban/tests/test_browser_kanban.py +150 -0
  26. pyjs_examples-0.0.1/kanban/tests/test_kanban.py +38 -0
  27. pyjs_examples-0.0.1/kanban/visual_test.py +108 -0
  28. pyjs_examples-0.0.1/nativescript_counter/README.md +31 -0
  29. pyjs_examples-0.0.1/nativescript_counter/__init__.py +5 -0
  30. pyjs_examples-0.0.1/nativescript_counter/__main__.py +17 -0
  31. pyjs_examples-0.0.1/nativescript_counter/counter.py +54 -0
  32. pyjs_examples-0.0.1/nativescript_counter/tests/test_android_counter.py +31 -0
  33. pyjs_examples-0.0.1/nativescript_counter/tests/test_counter.py +11 -0
  34. pyjs_examples-0.0.1/pyjs_examples.egg-info/PKG-INFO +71 -0
  35. pyjs_examples-0.0.1/pyjs_examples.egg-info/SOURCES.txt +140 -0
  36. pyjs_examples-0.0.1/pyjs_examples.egg-info/dependency_links.txt +1 -0
  37. pyjs_examples-0.0.1/pyjs_examples.egg-info/entry_points.txt +11 -0
  38. pyjs_examples-0.0.1/pyjs_examples.egg-info/requires.txt +4 -0
  39. pyjs_examples-0.0.1/pyjs_examples.egg-info/top_level.txt +1 -0
  40. pyjs_examples-0.0.1/pyproject.toml +101 -0
  41. pyjs_examples-0.0.1/samples/__init__.py +1 -0
  42. pyjs_examples-0.0.1/samples/state_counter/README.md +19 -0
  43. pyjs_examples-0.0.1/samples/state_counter/__init__.py +1 -0
  44. pyjs_examples-0.0.1/samples/state_counter/__main__.py +3 -0
  45. pyjs_examples-0.0.1/samples/state_counter/state_counter.py +60 -0
  46. pyjs_examples-0.0.1/samples/state_counter/tests/test_browser_state_counter.py +18 -0
  47. pyjs_examples-0.0.1/samples/state_counter/tests/test_state_counter.py +17 -0
  48. pyjs_examples-0.0.1/samples/ui_dialog/README.md +19 -0
  49. pyjs_examples-0.0.1/samples/ui_dialog/__init__.py +1 -0
  50. pyjs_examples-0.0.1/samples/ui_dialog/__main__.py +3 -0
  51. pyjs_examples-0.0.1/samples/ui_dialog/tests/test_browser_ui_dialog.py +21 -0
  52. pyjs_examples-0.0.1/samples/ui_dialog/tests/test_ui_dialog.py +10 -0
  53. pyjs_examples-0.0.1/samples/ui_dialog/ui_dialog.py +65 -0
  54. pyjs_examples-0.0.1/setup.cfg +4 -0
  55. pyjs_examples-0.0.1/space_invaders/README.md +45 -0
  56. pyjs_examples-0.0.1/space_invaders/__init__.py +1 -0
  57. pyjs_examples-0.0.1/space_invaders/__main__.py +3 -0
  58. pyjs_examples-0.0.1/space_invaders/space_invaders.py +1025 -0
  59. pyjs_examples-0.0.1/space_invaders/tests/renders/action_scene.png +0 -0
  60. pyjs_examples-0.0.1/space_invaders/tests/renders/initial_wave.png +0 -0
  61. pyjs_examples-0.0.1/space_invaders/tests/test_browser_game.py +86 -0
  62. pyjs_examples-0.0.1/space_invaders/tests/test_game.py +83 -0
  63. pyjs_examples-0.0.1/todomvc/README.md +50 -0
  64. pyjs_examples-0.0.1/todomvc/__init__.py +1 -0
  65. pyjs_examples-0.0.1/todomvc/__main__.py +12 -0
  66. pyjs_examples-0.0.1/todomvc/base.css +141 -0
  67. pyjs_examples-0.0.1/todomvc/index.css +393 -0
  68. pyjs_examples-0.0.1/todomvc/tests/test_browser_todomvc.py +145 -0
  69. pyjs_examples-0.0.1/todomvc/tests/test_todomvc.py +84 -0
  70. pyjs_examples-0.0.1/todomvc/todomvc.py +426 -0
  71. pyjs_examples-0.0.1/workspace/README.md +25 -0
  72. pyjs_examples-0.0.1/workspace/__init__.py +1 -0
  73. pyjs_examples-0.0.1/workspace/__main__.py +3 -0
  74. pyjs_examples-0.0.1/workspace/tests/test_browser_workspace.py +109 -0
  75. pyjs_examples-0.0.1/workspace/tests/test_workspace.py +27 -0
  76. pyjs_examples-0.0.1/workspace/workspace.py +749 -0
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyjs-examples
3
+ Version: 0.0.1
4
+ Summary: Official example applications for PyJS.
5
+ Author-email: Lex Berezhny <lex@damoti.com>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://pyjs.org
8
+ Project-URL: Source, https://github.com/pyjs/pyjs
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.14
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: pyjs==0.0.1
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest>=9.0; extra == "test"
24
+
25
+ # PyJS Examples
26
+
27
+ This directory is the source tree for the separately installable `pyjs-examples` distribution.
28
+ Setuptools maps this directory to the `pyjs_examples` import package, so the examples remain easy to
29
+ browse in the repository while installing as an ordinary top-level package.
30
+
31
+ From the repository root, install the core and examples from a source checkout:
32
+
33
+ ```bash
34
+ python -m pip install -e . -e ./examples
35
+ ```
36
+
37
+ An installed PyJS release provides the same package through the `examples` extra:
38
+
39
+ ```bash
40
+ python -m pip install "pyjs[examples]"
41
+ ```
42
+
43
+ Run any complete application as a module:
44
+
45
+ | Example | Command |
46
+ | --- | --- |
47
+ | [Calculator](calculator/) | `python -m pyjs_examples.calculator` |
48
+ | [Chatroom](chatroom/) | `python -m pyjs_examples.chatroom` |
49
+ | [Kanban board](kanban/) | `python -m pyjs_examples.kanban` |
50
+ | [NativeScript Counter](nativescript_counter/) | `python -m pyjs_examples.nativescript_counter` |
51
+ | [Space Invaders](space_invaders/) | `python -m pyjs_examples.space_invaders` |
52
+ | [TodoMVC](todomvc/) | `python -m pyjs_examples.todomvc` |
53
+ | [Component workspace](workspace/) | `python -m pyjs_examples.workspace` |
54
+ | [Component catalog](catalog.py) | `python -m pyjs_examples.catalog` |
55
+
56
+ List or copy examples using the core scaffolding command:
57
+
58
+ ```bash
59
+ pyjs template
60
+ pyjs template pyjs_examples.calculator
61
+ pyjs create my-calculator --template pyjs_examples.calculator
62
+ pyjs create --single-file my_counter.py --template pyjs_examples.counter
63
+ ```
64
+
65
+ The distribution registers modules and packages through the `pyjs.templates` entry-point group.
66
+ PyJS infers file and directory templates from normal Python module structure instead of hardcoding
67
+ a catalog.
68
+
69
+ The [focused samples](samples/) are small, executable sources intended for documentation excerpts
70
+ and `pyjs create PROJECT --template NAME`. The initial catalog includes
71
+ `pyjs_examples.samples.state_counter` and `pyjs_examples.samples.ui_dialog`.
@@ -0,0 +1,47 @@
1
+ # PyJS Examples
2
+
3
+ This directory is the source tree for the separately installable `pyjs-examples` distribution.
4
+ Setuptools maps this directory to the `pyjs_examples` import package, so the examples remain easy to
5
+ browse in the repository while installing as an ordinary top-level package.
6
+
7
+ From the repository root, install the core and examples from a source checkout:
8
+
9
+ ```bash
10
+ python -m pip install -e . -e ./examples
11
+ ```
12
+
13
+ An installed PyJS release provides the same package through the `examples` extra:
14
+
15
+ ```bash
16
+ python -m pip install "pyjs[examples]"
17
+ ```
18
+
19
+ Run any complete application as a module:
20
+
21
+ | Example | Command |
22
+ | --- | --- |
23
+ | [Calculator](calculator/) | `python -m pyjs_examples.calculator` |
24
+ | [Chatroom](chatroom/) | `python -m pyjs_examples.chatroom` |
25
+ | [Kanban board](kanban/) | `python -m pyjs_examples.kanban` |
26
+ | [NativeScript Counter](nativescript_counter/) | `python -m pyjs_examples.nativescript_counter` |
27
+ | [Space Invaders](space_invaders/) | `python -m pyjs_examples.space_invaders` |
28
+ | [TodoMVC](todomvc/) | `python -m pyjs_examples.todomvc` |
29
+ | [Component workspace](workspace/) | `python -m pyjs_examples.workspace` |
30
+ | [Component catalog](catalog.py) | `python -m pyjs_examples.catalog` |
31
+
32
+ List or copy examples using the core scaffolding command:
33
+
34
+ ```bash
35
+ pyjs template
36
+ pyjs template pyjs_examples.calculator
37
+ pyjs create my-calculator --template pyjs_examples.calculator
38
+ pyjs create --single-file my_counter.py --template pyjs_examples.counter
39
+ ```
40
+
41
+ The distribution registers modules and packages through the `pyjs.templates` entry-point group.
42
+ PyJS infers file and directory templates from normal Python module structure instead of hardcoding
43
+ a catalog.
44
+
45
+ The [focused samples](samples/) are small, executable sources intended for documentation excerpts
46
+ and `pyjs create PROJECT --template NAME`. The initial catalog includes
47
+ `pyjs_examples.samples.state_counter` and `pyjs_examples.samples.ui_dialog`.
@@ -0,0 +1 @@
1
+ """Official example applications and focused samples for PyJS."""
@@ -0,0 +1,55 @@
1
+ # Calculator Example
2
+
3
+ This project is a small but complete calculator application and the primary
4
+ example for pyjs browser-target testing.
5
+
6
+ It demonstrates:
7
+
8
+ - calculator state and behavior owned by a custom element
9
+ - typed `state()` fields serialized to DOM attributes and rendered with `@effect`
10
+ - a centered responsive custom-element UI
11
+ - digit and decimal entry
12
+ - addition, subtraction, multiplication, and division
13
+ - sign, percent, clear, and divide-by-zero behavior
14
+ - asynchronous result persistence through an HTTP POST route
15
+ - per-browser in-memory sessions that restore the last result after refresh
16
+ - browser tests that interact with the real DOM through button events
17
+
18
+ The example intentionally stays focused on calculator behavior. Reusable widget
19
+ coverage, including the dropdown menu and floating positioning, lives under
20
+ `tests/web/ui/`.
21
+
22
+ ## Run the calculator
23
+
24
+ From the repository root:
25
+
26
+ ```bash
27
+ .venv/bin/python -m pyjs_examples.calculator
28
+ ```
29
+
30
+ Open `http://127.0.0.1:8000/`. Completing a calculation with `=` saves its
31
+ result to the server. Refreshing the page restores that result through the
32
+ browser's session cookie. Sessions are intentionally in memory, so restarting
33
+ the server clears them.
34
+
35
+ ## Run the tests
36
+
37
+ From the repository root, run the Python target:
38
+
39
+ ```bash
40
+ .venv/bin/python -m pytest -q examples/calculator/tests
41
+ ```
42
+
43
+ Run the same model tests plus the browser-only UI tests in the browser:
44
+
45
+ ```bash
46
+ .venv/bin/python -m pytest -q examples/calculator/tests --pyjs-target=web
47
+ ```
48
+
49
+ The pytest plugin starts or reconnects to the browser-test daemon on
50
+ `127.0.0.1:8765`. To keep the daemon alive while iterating:
51
+
52
+ ```bash
53
+ .venv/bin/python -m pytest -q examples/calculator/tests --pyjs-target=web \
54
+ --pyjs-daemon-idle-seconds=0
55
+ ```
@@ -0,0 +1 @@
1
+ """A tested calculator with reactive DOM state and HTTP session persistence."""
@@ -0,0 +1,9 @@
1
+ from pyjs.server import serve
2
+
3
+ from .calculator import render, save_result
4
+
5
+ serve(
6
+ "pyjs_examples.calculator.calculator",
7
+ render=render,
8
+ routes={("POST", "/api/calculator/result"): save_result},
9
+ )
@@ -0,0 +1,325 @@
1
+ from pyjs import effect, js, nojs, state
2
+ from pyjs.web.domx import ProxyElement, button, ref, tag, tw
3
+ from pyjs.web.events import Event
4
+ from pyjs.web.network import fetch
5
+ from pyjs.web.ui import Widget
6
+
7
+
8
+ @js
9
+ class CalculatorKeyListener:
10
+ def __init__(self, app: CalculatorApp, kind: str, value: str):
11
+ self.app = app
12
+ self.kind = kind
13
+ self.value = value
14
+
15
+ @js(include=True)
16
+ async def handleEvent(self, event: Event):
17
+ self.app.press(self.kind, self.value)
18
+ if self.kind == "equals" and not self.app.error:
19
+ await self.app.persist_result()
20
+
21
+
22
+ @js
23
+ class CalculatorKey(ProxyElement):
24
+ def __init__(self, label: str, value: str, kind: str, wide: bool):
25
+ attributes = {
26
+ "aria-label": label,
27
+ "data-key": value,
28
+ "type": "button",
29
+ }
30
+ if wide:
31
+ self.element = button(
32
+ tw(
33
+ "col-span-2 h-12 rounded-xl bg-slate-100 px-5 text-left text-xl "
34
+ "font-medium text-slate-950 shadow-sm hover:bg-slate-200 "
35
+ "focus:outline-none focus:ring-2 focus:ring-slate-400"
36
+ ),
37
+ attributes,
38
+ label,
39
+ )
40
+ elif kind == "operator" or kind == "equals":
41
+ self.element = button(
42
+ tw(
43
+ "h-12 rounded-xl bg-amber-500 text-xl font-semibold text-white shadow-sm "
44
+ "hover:bg-amber-600 focus:outline-none focus:ring-2 focus:ring-amber-400"
45
+ ),
46
+ attributes,
47
+ label,
48
+ )
49
+ elif kind == "action":
50
+ self.element = button(
51
+ tw(
52
+ "h-12 rounded-xl bg-slate-200 text-lg font-medium text-slate-800 shadow-sm "
53
+ "hover:bg-slate-300 focus:outline-none focus:ring-2 focus:ring-slate-400"
54
+ ),
55
+ attributes,
56
+ label,
57
+ )
58
+ else:
59
+ self.element = button(
60
+ tw(
61
+ "h-12 rounded-xl bg-slate-100 text-xl font-medium text-slate-950 shadow-sm "
62
+ "hover:bg-slate-200 focus:outline-none focus:ring-2 focus:ring-slate-400"
63
+ ),
64
+ attributes,
65
+ label,
66
+ )
67
+
68
+ def bind(self, app: CalculatorApp, kind: str, value: str):
69
+ self.listener = CalculatorKeyListener(app, kind, value)
70
+ self.element.addEventListener("click", self.listener)
71
+
72
+
73
+ @js
74
+ class CalculatorApp(Widget):
75
+ display_value = state("0")
76
+ stored_value = state(0.0)
77
+ pending_operator = state("")
78
+ replace_display = state(True)
79
+ error = state(False)
80
+ save_url = state("")
81
+
82
+ def __init__(self, initial_value: str = "0", save_url: str = ""):
83
+ super().__init__()
84
+ self.display = ref(
85
+ tag(
86
+ "div",
87
+ tw(
88
+ "min-h-20 overflow-hidden rounded-xl bg-slate-950 px-5 py-4 text-right "
89
+ "font-mono text-3xl font-semibold tracking-tight text-white"
90
+ ),
91
+ {
92
+ "aria-live": "polite",
93
+ "data-testid": "calculator-display",
94
+ "role": "status",
95
+ },
96
+ initial_value,
97
+ )
98
+ )
99
+ self.display_value = initial_value
100
+ self.save_url = save_url
101
+
102
+ self.clear_button = ref(CalculatorKey("AC", "clear", "action", False))
103
+ self.clear_button.bind(self, "action", "clear")
104
+ self.sign_button = ref(CalculatorKey("±", "sign", "action", False))
105
+ self.sign_button.bind(self, "action", "sign")
106
+ self.percent_button = ref(CalculatorKey("%", "percent", "action", False))
107
+ self.percent_button.bind(self, "action", "percent")
108
+ self.divide_button = ref(CalculatorKey("÷", "/", "operator", False))
109
+ self.divide_button.bind(self, "operator", "/")
110
+ self.seven_button = ref(CalculatorKey("7", "7", "digit", False))
111
+ self.seven_button.bind(self, "digit", "7")
112
+ self.eight_button = ref(CalculatorKey("8", "8", "digit", False))
113
+ self.eight_button.bind(self, "digit", "8")
114
+ self.nine_button = ref(CalculatorKey("9", "9", "digit", False))
115
+ self.nine_button.bind(self, "digit", "9")
116
+ self.multiply_button = ref(CalculatorKey("×", "*", "operator", False))
117
+ self.multiply_button.bind(self, "operator", "*")
118
+ self.four_button = ref(CalculatorKey("4", "4", "digit", False))
119
+ self.four_button.bind(self, "digit", "4")
120
+ self.five_button = ref(CalculatorKey("5", "5", "digit", False))
121
+ self.five_button.bind(self, "digit", "5")
122
+ self.six_button = ref(CalculatorKey("6", "6", "digit", False))
123
+ self.six_button.bind(self, "digit", "6")
124
+ self.subtract_button = ref(CalculatorKey("−", "-", "operator", False))
125
+ self.subtract_button.bind(self, "operator", "-")
126
+ self.one_button = ref(CalculatorKey("1", "1", "digit", False))
127
+ self.one_button.bind(self, "digit", "1")
128
+ self.two_button = ref(CalculatorKey("2", "2", "digit", False))
129
+ self.two_button.bind(self, "digit", "2")
130
+ self.three_button = ref(CalculatorKey("3", "3", "digit", False))
131
+ self.three_button.bind(self, "digit", "3")
132
+ self.add_button = ref(CalculatorKey("+", "+", "operator", False))
133
+ self.add_button.bind(self, "operator", "+")
134
+ self.zero_button = ref(CalculatorKey("0", "0", "digit", True))
135
+ self.zero_button.bind(self, "digit", "0")
136
+ self.decimal_button = ref(CalculatorKey(".", ".", "decimal", False))
137
+ self.decimal_button.bind(self, "decimal", ".")
138
+ self.equals_button = ref(CalculatorKey("=", "=", "equals", False))
139
+ self.equals_button.bind(self, "equals", "=")
140
+ keys: list[CalculatorKey] = [
141
+ self.clear_button,
142
+ self.sign_button,
143
+ self.percent_button,
144
+ self.divide_button,
145
+ self.seven_button,
146
+ self.eight_button,
147
+ self.nine_button,
148
+ self.multiply_button,
149
+ self.four_button,
150
+ self.five_button,
151
+ self.six_button,
152
+ self.subtract_button,
153
+ self.one_button,
154
+ self.two_button,
155
+ self.three_button,
156
+ self.add_button,
157
+ self.zero_button,
158
+ self.decimal_button,
159
+ self.equals_button,
160
+ ]
161
+ card = tag(
162
+ "main",
163
+ tw("w-full max-w-sm rounded-3xl border border-slate-200 bg-white p-4 shadow-xl"),
164
+ {"data-testid": "calculator-card"},
165
+ tag(
166
+ "header",
167
+ tw("mb-4 flex items-end justify-between px-1"),
168
+ tag("h1", tw("text-lg font-semibold text-slate-950"), "Calculator"),
169
+ tag(
170
+ "span",
171
+ tw("text-xs font-medium uppercase tracking-widest text-slate-400"),
172
+ "PyJS",
173
+ ),
174
+ ),
175
+ self.display,
176
+ tag("div", tw("mt-4 grid grid-cols-4 gap-2"), *keys),
177
+ )
178
+ self.add(
179
+ tw("box-border flex min-h-screen items-center justify-center bg-slate-100 p-4"),
180
+ card,
181
+ )
182
+
183
+ @effect(display_value)
184
+ def render_display(self):
185
+ self.display.textContent = self.display_value
186
+
187
+ def clear(self):
188
+ self.display_value = "0"
189
+ self.stored_value = 0.0
190
+ self.pending_operator = ""
191
+ self.replace_display = True
192
+ self.error = False
193
+
194
+ def value(self) -> float:
195
+ if self.error:
196
+ return 0.0
197
+ return float(self.display_value)
198
+
199
+ def input_digit(self, digit: str):
200
+ if self.error:
201
+ self.clear()
202
+ if self.replace_display or self.display_value == "0":
203
+ self.display_value = digit
204
+ self.replace_display = False
205
+ else:
206
+ self.display_value += digit
207
+
208
+ def input_decimal(self):
209
+ if self.error:
210
+ self.clear()
211
+ if self.replace_display:
212
+ self.display_value = "0."
213
+ self.replace_display = False
214
+ elif len(self.display_value.split(".")) == 1:
215
+ self.display_value += "."
216
+
217
+ def toggle_sign(self):
218
+ value = self.display_value
219
+ if self.error or value == "0" or value == "0.0":
220
+ return
221
+ self.display_value = str(0.0 - float(value))
222
+
223
+ def percent(self):
224
+ if self.error:
225
+ return
226
+ self.display_value = str(float(self.display_value) / 100.0)
227
+ self.replace_display = True
228
+
229
+ def choose_operator(self, operator: str):
230
+ if self.error:
231
+ return
232
+ if self.pending_operator != "" and not self.replace_display:
233
+ self.calculate()
234
+ if self.error:
235
+ return
236
+ self.stored_value = float(self.display_value)
237
+ self.pending_operator = operator
238
+ self.replace_display = True
239
+
240
+ def calculate_result(self, left: float, right: float, operator: str) -> float:
241
+ if operator == "+":
242
+ return left + right
243
+ if operator == "-":
244
+ return left - right
245
+ if operator == "*":
246
+ return left * right
247
+ return left / right
248
+
249
+ def calculate(self) -> float:
250
+ operator = self.pending_operator
251
+ if self.error or operator == "":
252
+ return self.value()
253
+ right = float(self.display_value)
254
+ if operator == "/" and right == 0.0:
255
+ self.display_value = "Error"
256
+ self.pending_operator = ""
257
+ self.replace_display = True
258
+ self.error = True
259
+ return 0.0
260
+ result = self.calculate_result(self.stored_value, right, operator)
261
+ self.display_value = str(result)
262
+ self.stored_value = result
263
+ self.pending_operator = ""
264
+ self.replace_display = True
265
+ return result
266
+
267
+ async def persist_result(self):
268
+ if self.save_url == "":
269
+ return
270
+ await fetch(
271
+ self.save_url,
272
+ {
273
+ "method": "POST",
274
+ "headers": {"Content-Type": "text/plain"},
275
+ "body": self.display_value,
276
+ "keepalive": True,
277
+ },
278
+ )
279
+
280
+ def press(self, kind: str, value: str):
281
+ if kind == "digit":
282
+ self.input_digit(value)
283
+ elif kind == "decimal":
284
+ self.input_decimal()
285
+ elif kind == "operator":
286
+ self.choose_operator(value)
287
+ elif kind == "equals":
288
+ self.calculate()
289
+ elif value == "clear":
290
+ self.clear()
291
+ elif value == "sign":
292
+ self.toggle_sign()
293
+ else:
294
+ self.percent()
295
+
296
+
297
+ def main(initial_value: str = "0", save_url: str = ""):
298
+ return CalculatorApp(initial_value=initial_value, save_url=save_url)
299
+
300
+
301
+ @nojs
302
+ def render(request):
303
+ return main(request.session.get("last_result", "0"), "/api/calculator/result")
304
+
305
+
306
+ @nojs
307
+ def save_result(request):
308
+ import math
309
+
310
+ result = request.body.decode("utf-8")
311
+ if len(result) > 32:
312
+ raise ValueError("result must be a short numeric string")
313
+ if not math.isfinite(float(result)):
314
+ raise ValueError("result must be finite")
315
+ request.session["last_result"] = result
316
+
317
+
318
+ if __name__ == "__main__":
319
+ from pyjs.server import serve
320
+
321
+ serve(
322
+ "calculator",
323
+ render=render,
324
+ routes={("POST", "/api/calculator/result"): save_result},
325
+ )
@@ -0,0 +1,67 @@
1
+ import pytest
2
+ from pyjs import js
3
+ from pyjs.testing.web import mount
4
+ from pyjs.web.events import Event
5
+ from pyjs.web.html import HTMLButtonElement
6
+ from pyjs_examples.calculator.calculator import CalculatorApp
7
+
8
+ pytestmark = pytest.mark.pyjs_web
9
+
10
+
11
+ @js
12
+ def click(button: HTMLButtonElement):
13
+ button.dispatchEvent(Event("click"))
14
+
15
+
16
+ @js
17
+ def mounted_calculator() -> CalculatorApp:
18
+ app = CalculatorApp()
19
+ mount(app)
20
+ return app
21
+
22
+
23
+ @js
24
+ def test_calculator_renders_a_complete_keypad():
25
+ app = mounted_calculator()
26
+
27
+ assert len(app.querySelectorAll("[data-key]")) == 19
28
+ assert app.display.textContent.strip() == "0"
29
+
30
+
31
+ @js
32
+ def test_calculator_buttons_complete_an_expression():
33
+ app = mounted_calculator()
34
+ click(app.one_button.element)
35
+ click(app.two_button.element)
36
+ click(app.add_button.element)
37
+ click(app.seven_button.element)
38
+ click(app.equals_button.element)
39
+
40
+ assert float(app.display.textContent.strip()) == 19.0
41
+
42
+
43
+ @js
44
+ def test_calculator_supports_decimal_sign_percent_and_clear():
45
+ app = mounted_calculator()
46
+ click(app.five_button.element)
47
+ click(app.zero_button.element)
48
+ click(app.percent_button.element)
49
+ assert float(app.display.textContent.strip()) == 0.5
50
+
51
+ click(app.sign_button.element)
52
+ assert float(app.display.textContent.strip()) == -0.5
53
+
54
+ click(app.clear_button.element)
55
+ assert app.display.textContent.strip() == "0"
56
+
57
+
58
+ @js
59
+ def test_calculator_visual_showcase():
60
+ app = mounted_calculator()
61
+ click(app.one_button.element)
62
+ click(app.two_button.element)
63
+ click(app.multiply_button.element)
64
+ click(app.three_button.element)
65
+ click(app.equals_button.element)
66
+
67
+ assert float(app.display.textContent.strip()) == 36.0
@@ -0,0 +1,72 @@
1
+ from pyjs import js
2
+ from pyjs_examples.calculator.calculator import CalculatorApp
3
+
4
+
5
+ @js
6
+ def test_enters_whole_and_decimal_numbers():
7
+ calculator = CalculatorApp()
8
+ calculator.input_digit("1")
9
+ calculator.input_digit("2")
10
+ calculator.input_decimal()
11
+ calculator.input_digit("5")
12
+
13
+ assert calculator.value() == 12.5
14
+
15
+
16
+ @js
17
+ def test_performs_the_four_arithmetic_operations():
18
+ calculator = CalculatorApp()
19
+ calculator.input_digit("8")
20
+ calculator.choose_operator("+")
21
+ calculator.input_digit("2")
22
+ assert calculator.calculate() == 10.0
23
+
24
+ calculator.choose_operator("-")
25
+ calculator.input_digit("3")
26
+ assert calculator.calculate() == 7.0
27
+
28
+ calculator.choose_operator("*")
29
+ calculator.input_digit("6")
30
+ assert calculator.calculate() == 42.0
31
+
32
+ calculator.choose_operator("/")
33
+ calculator.input_digit("2")
34
+ assert calculator.calculate() == 21.0
35
+
36
+
37
+ @js
38
+ def test_supports_sign_percent_clear_and_divide_by_zero():
39
+ calculator = CalculatorApp()
40
+ calculator.input_digit("5")
41
+ calculator.input_digit("0")
42
+ calculator.percent()
43
+ assert calculator.value() == 0.5
44
+
45
+ calculator.toggle_sign()
46
+ assert calculator.value() == -0.5
47
+
48
+ calculator.clear()
49
+ assert calculator.value() == 0.0
50
+
51
+ calculator.input_digit("9")
52
+ calculator.choose_operator("/")
53
+ calculator.input_digit("0")
54
+ calculator.calculate()
55
+ assert calculator.display_value == "Error"
56
+ assert calculator.error
57
+
58
+
59
+ @js
60
+ def test_initial_and_calculation_state_is_stored_on_the_custom_element():
61
+ calculator = CalculatorApp(initial_value="12.0")
62
+
63
+ assert calculator.getAttribute("display-value") == "12.0"
64
+ calculator.choose_operator("+")
65
+ calculator.input_digit("3")
66
+
67
+ stored_value = calculator.getAttribute("stored-value")
68
+ assert stored_value is not None
69
+ assert float(stored_value) == 12.0
70
+ assert calculator.getAttribute("pending-operator") == "+"
71
+ assert not calculator.hasAttribute("replace-display")
72
+ assert not calculator.hasAttribute("error")