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.
- pyjs_examples-0.0.1/PKG-INFO +71 -0
- pyjs_examples-0.0.1/README.md +47 -0
- pyjs_examples-0.0.1/__init__.py +1 -0
- pyjs_examples-0.0.1/calculator/README.md +55 -0
- pyjs_examples-0.0.1/calculator/__init__.py +1 -0
- pyjs_examples-0.0.1/calculator/__main__.py +9 -0
- pyjs_examples-0.0.1/calculator/calculator.py +325 -0
- pyjs_examples-0.0.1/calculator/tests/test_browser_calculator.py +67 -0
- pyjs_examples-0.0.1/calculator/tests/test_calculator.py +72 -0
- pyjs_examples-0.0.1/catalog.py +1433 -0
- pyjs_examples-0.0.1/chatroom/README.md +50 -0
- pyjs_examples-0.0.1/chatroom/__init__.py +1 -0
- pyjs_examples-0.0.1/chatroom/__main__.py +12 -0
- pyjs_examples-0.0.1/chatroom/chat_server.py +156 -0
- pyjs_examples-0.0.1/chatroom/chatroom.py +704 -0
- pyjs_examples-0.0.1/chatroom/tests/test_browser_chatroom.py +59 -0
- pyjs_examples-0.0.1/chatroom/tests/test_chat_server.py +53 -0
- pyjs_examples-0.0.1/chatroom/visual_test.py +156 -0
- pyjs_examples-0.0.1/counter.py +73 -0
- pyjs_examples-0.0.1/kanban/README.md +31 -0
- pyjs_examples-0.0.1/kanban/__init__.py +1 -0
- pyjs_examples-0.0.1/kanban/__main__.py +8 -0
- pyjs_examples-0.0.1/kanban/kanban.py +581 -0
- pyjs_examples-0.0.1/kanban/tests/renders/kanban-board.png +0 -0
- pyjs_examples-0.0.1/kanban/tests/test_browser_kanban.py +150 -0
- pyjs_examples-0.0.1/kanban/tests/test_kanban.py +38 -0
- pyjs_examples-0.0.1/kanban/visual_test.py +108 -0
- pyjs_examples-0.0.1/nativescript_counter/README.md +31 -0
- pyjs_examples-0.0.1/nativescript_counter/__init__.py +5 -0
- pyjs_examples-0.0.1/nativescript_counter/__main__.py +17 -0
- pyjs_examples-0.0.1/nativescript_counter/counter.py +54 -0
- pyjs_examples-0.0.1/nativescript_counter/tests/test_android_counter.py +31 -0
- pyjs_examples-0.0.1/nativescript_counter/tests/test_counter.py +11 -0
- pyjs_examples-0.0.1/pyjs_examples.egg-info/PKG-INFO +71 -0
- pyjs_examples-0.0.1/pyjs_examples.egg-info/SOURCES.txt +140 -0
- pyjs_examples-0.0.1/pyjs_examples.egg-info/dependency_links.txt +1 -0
- pyjs_examples-0.0.1/pyjs_examples.egg-info/entry_points.txt +11 -0
- pyjs_examples-0.0.1/pyjs_examples.egg-info/requires.txt +4 -0
- pyjs_examples-0.0.1/pyjs_examples.egg-info/top_level.txt +1 -0
- pyjs_examples-0.0.1/pyproject.toml +101 -0
- pyjs_examples-0.0.1/samples/__init__.py +1 -0
- pyjs_examples-0.0.1/samples/state_counter/README.md +19 -0
- pyjs_examples-0.0.1/samples/state_counter/__init__.py +1 -0
- pyjs_examples-0.0.1/samples/state_counter/__main__.py +3 -0
- pyjs_examples-0.0.1/samples/state_counter/state_counter.py +60 -0
- pyjs_examples-0.0.1/samples/state_counter/tests/test_browser_state_counter.py +18 -0
- pyjs_examples-0.0.1/samples/state_counter/tests/test_state_counter.py +17 -0
- pyjs_examples-0.0.1/samples/ui_dialog/README.md +19 -0
- pyjs_examples-0.0.1/samples/ui_dialog/__init__.py +1 -0
- pyjs_examples-0.0.1/samples/ui_dialog/__main__.py +3 -0
- pyjs_examples-0.0.1/samples/ui_dialog/tests/test_browser_ui_dialog.py +21 -0
- pyjs_examples-0.0.1/samples/ui_dialog/tests/test_ui_dialog.py +10 -0
- pyjs_examples-0.0.1/samples/ui_dialog/ui_dialog.py +65 -0
- pyjs_examples-0.0.1/setup.cfg +4 -0
- pyjs_examples-0.0.1/space_invaders/README.md +45 -0
- pyjs_examples-0.0.1/space_invaders/__init__.py +1 -0
- pyjs_examples-0.0.1/space_invaders/__main__.py +3 -0
- pyjs_examples-0.0.1/space_invaders/space_invaders.py +1025 -0
- pyjs_examples-0.0.1/space_invaders/tests/renders/action_scene.png +0 -0
- pyjs_examples-0.0.1/space_invaders/tests/renders/initial_wave.png +0 -0
- pyjs_examples-0.0.1/space_invaders/tests/test_browser_game.py +86 -0
- pyjs_examples-0.0.1/space_invaders/tests/test_game.py +83 -0
- pyjs_examples-0.0.1/todomvc/README.md +50 -0
- pyjs_examples-0.0.1/todomvc/__init__.py +1 -0
- pyjs_examples-0.0.1/todomvc/__main__.py +12 -0
- pyjs_examples-0.0.1/todomvc/base.css +141 -0
- pyjs_examples-0.0.1/todomvc/index.css +393 -0
- pyjs_examples-0.0.1/todomvc/tests/test_browser_todomvc.py +145 -0
- pyjs_examples-0.0.1/todomvc/tests/test_todomvc.py +84 -0
- pyjs_examples-0.0.1/todomvc/todomvc.py +426 -0
- pyjs_examples-0.0.1/workspace/README.md +25 -0
- pyjs_examples-0.0.1/workspace/__init__.py +1 -0
- pyjs_examples-0.0.1/workspace/__main__.py +3 -0
- pyjs_examples-0.0.1/workspace/tests/test_browser_workspace.py +109 -0
- pyjs_examples-0.0.1/workspace/tests/test_workspace.py +27 -0
- 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,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")
|