pyjs-examples 0.0.2__tar.gz → 0.4.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.
Files changed (137) hide show
  1. pyjs_examples-0.4.0/LICENSE +28 -0
  2. pyjs_examples-0.4.0/PKG-INFO +108 -0
  3. pyjs_examples-0.4.0/README.md +73 -0
  4. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/calculator/README.md +21 -5
  5. pyjs_examples-0.4.0/calculator/__init__.py +1 -0
  6. pyjs_examples-0.4.0/calculator/__main__.py +21 -0
  7. pyjs_examples-0.4.0/calculator/android.py +225 -0
  8. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/calculator/calculator.py +15 -17
  9. pyjs_examples-0.4.0/calculator/tests/test_android_calculator.py +34 -0
  10. pyjs_examples-0.4.0/calculator/tests/test_android_calculator_model.py +26 -0
  11. pyjs_examples-0.4.0/chat/README.md +76 -0
  12. {pyjs_examples-0.0.2/chatroom → pyjs_examples-0.4.0/chat}/__main__.py +3 -3
  13. pyjs_examples-0.4.0/chat/chat.py +1165 -0
  14. {pyjs_examples-0.0.2/chatroom → pyjs_examples-0.4.0/chat}/chat_server.py +79 -36
  15. pyjs_examples-0.4.0/chat/protocol.py +28 -0
  16. pyjs_examples-0.4.0/chat/tests/test_browser_chat.py +149 -0
  17. {pyjs_examples-0.0.2/chatroom → pyjs_examples-0.4.0/chat}/tests/test_chat_server.py +50 -2
  18. {pyjs_examples-0.0.2/chatroom → pyjs_examples-0.4.0/chat}/visual_test.py +68 -13
  19. pyjs_examples-0.4.0/components/README.md +38 -0
  20. pyjs_examples-0.4.0/components/__init__.py +1 -0
  21. pyjs_examples-0.4.0/components/__main__.py +23 -0
  22. pyjs_examples-0.4.0/components/app.py +95 -0
  23. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0/components}/catalog.py +359 -56
  24. pyjs_examples-0.4.0/components/catalog_sources.py +301 -0
  25. pyjs_examples-0.4.0/components/tests/test_browser_navigation.py +58 -0
  26. {pyjs_examples-0.0.2/workspace → pyjs_examples-0.4.0/components}/tests/test_browser_workspace.py +1 -1
  27. pyjs_examples-0.4.0/components/tests/test_components.py +68 -0
  28. {pyjs_examples-0.0.2/workspace → pyjs_examples-0.4.0/components}/tests/test_workspace.py +3 -1
  29. {pyjs_examples-0.0.2/workspace → pyjs_examples-0.4.0/components}/workspace.py +10 -14
  30. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/counter.py +1 -1
  31. pyjs_examples-0.4.0/notes/README.md +48 -0
  32. pyjs_examples-0.4.0/notes/__init__.py +5 -0
  33. pyjs_examples-0.4.0/notes/__main__.py +18 -0
  34. pyjs_examples-0.4.0/notes/mobile_assets/fonts/LICENSE +202 -0
  35. pyjs_examples-0.4.0/notes/mobile_assets/fonts/MaterialIcons-Regular.ttf +0 -0
  36. pyjs_examples-0.4.0/notes/notes.py +1016 -0
  37. pyjs_examples-0.4.0/notes/tests/renders/android/notes-actions.png +0 -0
  38. pyjs_examples-0.4.0/notes/tests/renders/android/notes-checklist-editor.png +0 -0
  39. pyjs_examples-0.4.0/notes/tests/renders/android/notes-home.png +0 -0
  40. pyjs_examples-0.4.0/notes/tests/renders/android/notes-list.png +0 -0
  41. pyjs_examples-0.4.0/notes/tests/renders/android/notes-navigation.png +0 -0
  42. pyjs_examples-0.4.0/notes/tests/test_android_notes.py +116 -0
  43. pyjs_examples-0.4.0/notes/tests/test_browser_web_notes.py +149 -0
  44. pyjs_examples-0.4.0/notes/tests/test_notes.py +181 -0
  45. pyjs_examples-0.4.0/notes/tests/test_web_notes.py +56 -0
  46. {pyjs_examples-0.0.2/kanban → pyjs_examples-0.4.0/notes}/visual_test.py +29 -25
  47. pyjs_examples-0.0.2/kanban/kanban.py → pyjs_examples-0.4.0/notes/web.py +159 -89
  48. pyjs_examples-0.4.0/pyjs_examples.egg-info/PKG-INFO +108 -0
  49. pyjs_examples-0.4.0/pyjs_examples.egg-info/SOURCES.txt +157 -0
  50. pyjs_examples-0.4.0/pyjs_examples.egg-info/entry_points.txt +7 -0
  51. pyjs_examples-0.4.0/pyjs_examples.egg-info/requires.txt +14 -0
  52. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/pyproject.toml +44 -37
  53. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/README.md +8 -7
  54. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/__main__.py +1 -1
  55. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/space_invaders.py +69 -18
  56. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/tests/test_browser_game.py +27 -2
  57. pyjs_examples-0.4.0/store/README.md +134 -0
  58. pyjs_examples-0.4.0/store/__init__.py +1 -0
  59. pyjs_examples-0.4.0/store/__main__.py +67 -0
  60. pyjs_examples-0.4.0/store/api.py +584 -0
  61. pyjs_examples-0.4.0/store/apps.py +7 -0
  62. pyjs_examples-0.4.0/store/core.py +137 -0
  63. pyjs_examples-0.4.0/store/fixtures/store.json +258 -0
  64. pyjs_examples-0.4.0/store/models.py +93 -0
  65. pyjs_examples-0.4.0/store/server.py +192 -0
  66. pyjs_examples-0.4.0/store/settings.py +23 -0
  67. pyjs_examples-0.4.0/store/store.py +1776 -0
  68. pyjs_examples-0.4.0/store/tests/conftest.py +33 -0
  69. pyjs_examples-0.4.0/store/tests/test_browser_store.py +533 -0
  70. pyjs_examples-0.4.0/store/tests/test_store.py +987 -0
  71. pyjs_examples-0.4.0/store/urls.py +28 -0
  72. pyjs_examples-0.4.0/store/visual_test.py +129 -0
  73. pyjs_examples-0.0.2/PKG-INFO +0 -72
  74. pyjs_examples-0.0.2/README.md +0 -48
  75. pyjs_examples-0.0.2/calculator/__init__.py +0 -1
  76. pyjs_examples-0.0.2/calculator/__main__.py +0 -15
  77. pyjs_examples-0.0.2/chatroom/README.md +0 -50
  78. pyjs_examples-0.0.2/chatroom/chatroom.py +0 -707
  79. pyjs_examples-0.0.2/chatroom/tests/test_browser_chatroom.py +0 -63
  80. pyjs_examples-0.0.2/kanban/README.md +0 -31
  81. pyjs_examples-0.0.2/kanban/__init__.py +0 -1
  82. pyjs_examples-0.0.2/kanban/__main__.py +0 -8
  83. pyjs_examples-0.0.2/kanban/tests/test_browser_kanban.py +0 -155
  84. pyjs_examples-0.0.2/kanban/tests/test_kanban.py +0 -38
  85. pyjs_examples-0.0.2/nativescript_counter/README.md +0 -31
  86. pyjs_examples-0.0.2/nativescript_counter/__init__.py +0 -5
  87. pyjs_examples-0.0.2/nativescript_counter/__main__.py +0 -17
  88. pyjs_examples-0.0.2/nativescript_counter/counter.py +0 -54
  89. pyjs_examples-0.0.2/nativescript_counter/tests/test_android_counter.py +0 -31
  90. pyjs_examples-0.0.2/nativescript_counter/tests/test_counter.py +0 -11
  91. pyjs_examples-0.0.2/navigation/README.md +0 -20
  92. pyjs_examples-0.0.2/navigation/__init__.py +0 -1
  93. pyjs_examples-0.0.2/navigation/__main__.py +0 -49
  94. pyjs_examples-0.0.2/navigation/navigation.py +0 -800
  95. pyjs_examples-0.0.2/navigation/tests/test_browser_navigation.py +0 -83
  96. pyjs_examples-0.0.2/navigation/tests/test_navigation.py +0 -59
  97. pyjs_examples-0.0.2/pyjs_examples.egg-info/PKG-INFO +0 -72
  98. pyjs_examples-0.0.2/pyjs_examples.egg-info/SOURCES.txt +0 -154
  99. pyjs_examples-0.0.2/pyjs_examples.egg-info/entry_points.txt +0 -12
  100. pyjs_examples-0.0.2/pyjs_examples.egg-info/requires.txt +0 -4
  101. pyjs_examples-0.0.2/samples/__init__.py +0 -1
  102. pyjs_examples-0.0.2/samples/state_counter/README.md +0 -19
  103. pyjs_examples-0.0.2/samples/state_counter/__init__.py +0 -1
  104. pyjs_examples-0.0.2/samples/state_counter/__main__.py +0 -3
  105. pyjs_examples-0.0.2/samples/state_counter/state_counter.py +0 -60
  106. pyjs_examples-0.0.2/samples/state_counter/tests/test_browser_state_counter.py +0 -18
  107. pyjs_examples-0.0.2/samples/state_counter/tests/test_state_counter.py +0 -17
  108. pyjs_examples-0.0.2/samples/ui_dialog/README.md +0 -19
  109. pyjs_examples-0.0.2/samples/ui_dialog/__init__.py +0 -1
  110. pyjs_examples-0.0.2/samples/ui_dialog/__main__.py +0 -3
  111. pyjs_examples-0.0.2/samples/ui_dialog/tests/test_browser_ui_dialog.py +0 -21
  112. pyjs_examples-0.0.2/samples/ui_dialog/tests/test_ui_dialog.py +0 -10
  113. pyjs_examples-0.0.2/samples/ui_dialog/ui_dialog.py +0 -65
  114. pyjs_examples-0.0.2/todomvc/README.md +0 -50
  115. pyjs_examples-0.0.2/todomvc/__init__.py +0 -1
  116. pyjs_examples-0.0.2/todomvc/__main__.py +0 -12
  117. pyjs_examples-0.0.2/todomvc/base.css +0 -141
  118. pyjs_examples-0.0.2/todomvc/index.css +0 -393
  119. pyjs_examples-0.0.2/todomvc/tests/test_browser_todomvc.py +0 -152
  120. pyjs_examples-0.0.2/todomvc/tests/test_todomvc.py +0 -84
  121. pyjs_examples-0.0.2/todomvc/todomvc.py +0 -428
  122. pyjs_examples-0.0.2/workspace/README.md +0 -25
  123. pyjs_examples-0.0.2/workspace/__init__.py +0 -1
  124. pyjs_examples-0.0.2/workspace/__main__.py +0 -3
  125. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/__init__.py +0 -0
  126. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/calculator/tests/test_browser_calculator.py +0 -0
  127. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/calculator/tests/test_calculator.py +0 -0
  128. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/calculator/tests/test_pwa.py +0 -0
  129. {pyjs_examples-0.0.2/chatroom → pyjs_examples-0.4.0/chat}/__init__.py +0 -0
  130. /pyjs_examples-0.0.2/kanban/tests/renders/kanban-board.png → /pyjs_examples-0.4.0/notes/tests/renders/notes-board.png +0 -0
  131. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/pyjs_examples.egg-info/dependency_links.txt +0 -0
  132. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/pyjs_examples.egg-info/top_level.txt +0 -0
  133. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/setup.cfg +0 -0
  134. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/__init__.py +0 -0
  135. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/tests/renders/action_scene.png +0 -0
  136. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/tests/renders/initial_wave.png +0 -0
  137. {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/tests/test_game.py +0 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Lex Berezhny
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyjs-examples
3
+ Version: 0.4.0
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://github.com/pyjs/pyjs
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
+ License-File: LICENSE
22
+ Requires-Dist: pyjs==0.4.0
23
+ Provides-Extra: store
24
+ Requires-Dist: django<6.2,>=6.0; extra == "store"
25
+ Requires-Dist: pydantic<3,>=2; extra == "store"
26
+ Requires-Dist: pyjs-extras==0.4.0; extra == "store"
27
+ Requires-Dist: uvicorn<1,>=0.52; extra == "store"
28
+ Provides-Extra: test
29
+ Requires-Dist: django<6.2,>=6.0; extra == "test"
30
+ Requires-Dist: pydantic<3,>=2; extra == "test"
31
+ Requires-Dist: pyjs-extras==0.4.0; extra == "test"
32
+ Requires-Dist: pytest>=9.0; extra == "test"
33
+ Requires-Dist: uvicorn<1,>=0.52; extra == "test"
34
+ Dynamic: license-file
35
+
36
+ # PyJS Examples
37
+
38
+ This directory is the source tree for the separately installable
39
+ `pyjs-examples` distribution. Setuptools maps it to the `pyjs_examples` import
40
+ package while keeping each application easy to browse in the repository.
41
+
42
+ Install the core and examples from a source checkout:
43
+
44
+ ```bash
45
+ uv sync
46
+ ```
47
+
48
+ An installed release provides the examples through an extra:
49
+
50
+ ```bash
51
+ python -m pip install "pyjs[examples]"
52
+ ```
53
+
54
+ ## Application responsibilities
55
+
56
+ The examples progress from small programs to composed, full-stack systems.
57
+ Features may overlap when that makes an application more realistic.
58
+
59
+ | Example | Role | Principal PyJS coverage |
60
+ | --- | --- | --- |
61
+ | [Counter](counter.py) | Smallest app | DOM, events, one-file tests |
62
+ | [Calculator](calculator/) | Universal app | Web, PWA, Android, SSR, state |
63
+ | [Components](components/) | UI reference | Widgets, forms, navigation |
64
+ | [Notes](notes/) | Personal app | Web, Android, drag/drop, restore |
65
+ | [Chat](chat/) | Real-time starter | WebSockets, multi-window, plans |
66
+ | [Store](store/) | Business app | Django, JSON:API, SSE, themes, cursor feeds |
67
+ | [Space Invaders](space_invaders/) | GPU game | WebGPU, input, render tests |
68
+
69
+ Run an application with its module name, for example:
70
+
71
+ ```bash
72
+ python -m pyjs_examples.calculator
73
+ python -m pyjs_examples.components
74
+ python -m pyjs_examples.notes
75
+ python -m pyjs_examples.chat
76
+ python -m pyjs_examples.store
77
+ ```
78
+
79
+ Install `pyjs-examples[store]` before running Store from a release package.
80
+
81
+ Calculator and Notes accept `--android` to build and launch their NativeScript
82
+ versions. Their default command serves the web version.
83
+
84
+ Components is also the source and test surface for the planned website
85
+ component reference. It combines the former catalog, workspace, navigation,
86
+ and focused dialog sample in one application.
87
+
88
+ Notes combines the former Kanban board with the mobile notes application. It
89
+ uses DOM-owned ordering, drag/drop, server restoration, and visual tests.
90
+
91
+ Store is the canonical Django and typed client/server example. It also owns the
92
+ themability story, signed cursor feeds, infinite scrolling, and data-driven
93
+ charts.
94
+
95
+ ## Templates
96
+
97
+ List, inspect, or copy examples using the core scaffolding command:
98
+
99
+ ```bash
100
+ pyjs template
101
+ pyjs template pyjs_examples.calculator
102
+ pyjs create my-calculator --template pyjs_examples.calculator
103
+ pyjs create --single-file my_counter.py --template pyjs_examples.counter
104
+ ```
105
+
106
+ The distribution registers modules and packages through the `pyjs.templates`
107
+ entry-point group. PyJS infers file and directory templates from normal Python
108
+ module structure instead of hardcoding a catalog.
@@ -0,0 +1,73 @@
1
+ # PyJS Examples
2
+
3
+ This directory is the source tree for the separately installable
4
+ `pyjs-examples` distribution. Setuptools maps it to the `pyjs_examples` import
5
+ package while keeping each application easy to browse in the repository.
6
+
7
+ Install the core and examples from a source checkout:
8
+
9
+ ```bash
10
+ uv sync
11
+ ```
12
+
13
+ An installed release provides the examples through an extra:
14
+
15
+ ```bash
16
+ python -m pip install "pyjs[examples]"
17
+ ```
18
+
19
+ ## Application responsibilities
20
+
21
+ The examples progress from small programs to composed, full-stack systems.
22
+ Features may overlap when that makes an application more realistic.
23
+
24
+ | Example | Role | Principal PyJS coverage |
25
+ | --- | --- | --- |
26
+ | [Counter](counter.py) | Smallest app | DOM, events, one-file tests |
27
+ | [Calculator](calculator/) | Universal app | Web, PWA, Android, SSR, state |
28
+ | [Components](components/) | UI reference | Widgets, forms, navigation |
29
+ | [Notes](notes/) | Personal app | Web, Android, drag/drop, restore |
30
+ | [Chat](chat/) | Real-time starter | WebSockets, multi-window, plans |
31
+ | [Store](store/) | Business app | Django, JSON:API, SSE, themes, cursor feeds |
32
+ | [Space Invaders](space_invaders/) | GPU game | WebGPU, input, render tests |
33
+
34
+ Run an application with its module name, for example:
35
+
36
+ ```bash
37
+ python -m pyjs_examples.calculator
38
+ python -m pyjs_examples.components
39
+ python -m pyjs_examples.notes
40
+ python -m pyjs_examples.chat
41
+ python -m pyjs_examples.store
42
+ ```
43
+
44
+ Install `pyjs-examples[store]` before running Store from a release package.
45
+
46
+ Calculator and Notes accept `--android` to build and launch their NativeScript
47
+ versions. Their default command serves the web version.
48
+
49
+ Components is also the source and test surface for the planned website
50
+ component reference. It combines the former catalog, workspace, navigation,
51
+ and focused dialog sample in one application.
52
+
53
+ Notes combines the former Kanban board with the mobile notes application. It
54
+ uses DOM-owned ordering, drag/drop, server restoration, and visual tests.
55
+
56
+ Store is the canonical Django and typed client/server example. It also owns the
57
+ themability story, signed cursor feeds, infinite scrolling, and data-driven
58
+ charts.
59
+
60
+ ## Templates
61
+
62
+ List, inspect, or copy examples using the core scaffolding command:
63
+
64
+ ```bash
65
+ pyjs template
66
+ pyjs template pyjs_examples.calculator
67
+ pyjs create my-calculator --template pyjs_examples.calculator
68
+ pyjs create --single-file my_counter.py --template pyjs_examples.counter
69
+ ```
70
+
71
+ The distribution registers modules and packages through the `pyjs.templates`
72
+ entry-point group. PyJS infers file and directory templates from normal Python
73
+ module structure instead of hardcoding a catalog.
@@ -6,7 +6,8 @@ example for pyjs browser-target testing.
6
6
  It demonstrates:
7
7
 
8
8
  - calculator state and behavior owned by a custom element
9
- - typed `state()` fields serialized to DOM attributes and rendered with `@effect`
9
+ - typed `state()` fields serialized to DOM attributes and rendered with
10
+ `@effect`
10
11
  - a centered responsive custom-element UI
11
12
  - digit and decimal entry
12
13
  - addition, subtraction, multiplication, and division
@@ -14,6 +15,7 @@ It demonstrates:
14
15
  - asynchronous result persistence through an HTTP POST route
15
16
  - per-browser in-memory sessions that restore the last result after refresh
16
17
  - a Python-authored service worker with cache-first offline application loading
18
+ - a native Android implementation using the same arithmetic primitive
17
19
  - browser tests that interact with the real DOM through button events
18
20
 
19
21
  The example intentionally stays focused on calculator behavior. Reusable widget
@@ -25,7 +27,7 @@ coverage, including the dropdown menu and floating positioning, lives under
25
27
  From the repository root:
26
28
 
27
29
  ```bash
28
- .venv/bin/python -m pyjs_examples.calculator
30
+ python -m pyjs_examples.calculator
29
31
  ```
30
32
 
31
33
  Open `http://127.0.0.1:8000/`. Completing a calculation with `=` saves its
@@ -35,24 +37,38 @@ the server clears them. The app also registers `/calculator-serviceworker.js`.
35
37
  After its first controlled reload, the calculator can load and calculate while
36
38
  the browser is offline.
37
39
 
40
+ Run the native Android version on an existing emulator, or let the managed
41
+ NativeScript tooling start its configured device:
42
+
43
+ ```console
44
+ python -m pyjs_examples.calculator --android
45
+ ```
46
+
38
47
  ## Run the tests
39
48
 
40
49
  From the repository root, run the Python target:
41
50
 
42
51
  ```bash
43
- .venv/bin/python -m pytest -q examples/calculator/tests
52
+ pytest -q examples/calculator/tests
44
53
  ```
45
54
 
46
55
  Run the same model tests plus the browser-only UI tests in the browser:
47
56
 
48
57
  ```bash
49
- .venv/bin/python -m pytest -q examples/calculator/tests --pyjs-target=web
58
+ pytest -q examples/calculator/tests --pyjs-target=web
50
59
  ```
51
60
 
52
61
  The pytest plugin starts or reconnects to the browser-test daemon on
53
62
  `127.0.0.1:8765`. To keep the daemon alive while iterating:
54
63
 
55
64
  ```bash
56
- .venv/bin/python -m pytest -q examples/calculator/tests --pyjs-target=web \
65
+ pytest -q examples/calculator/tests --pyjs-target=web \
57
66
  --pyjs-daemon-idle-seconds=0
58
67
  ```
68
+
69
+ Run the Android interaction test with:
70
+
71
+ ```console
72
+ pytest -q examples/calculator/tests/test_android_calculator.py \
73
+ --pyjs-target=android --pyjs-android-avd=Routine_Test
74
+ ```
@@ -0,0 +1 @@
1
+ """A tested universal calculator for the Web, PWA, and Android targets."""
@@ -0,0 +1,21 @@
1
+ import os
2
+ import sys
3
+
4
+ from pyjs.server.dev import serve
5
+
6
+ from .calculator import render, save_result, service_worker_javascript
7
+
8
+ if "--android" in sys.argv:
9
+ from .android import run_android
10
+
11
+ run_android()
12
+ else:
13
+ serve(
14
+ "pyjs_examples.calculator.calculator",
15
+ render=render,
16
+ routes={
17
+ ("POST", "/api/calculator/result"): save_result,
18
+ ("GET", "/calculator-serviceworker.js"): service_worker_javascript,
19
+ },
20
+ port=int(os.environ.get("PYJS_PORT", "8000")),
21
+ )
@@ -0,0 +1,225 @@
1
+ from pyjs import js
2
+ from pyjs.nativescript import (
3
+ Application,
4
+ Button,
5
+ EventData,
6
+ GridLayout,
7
+ Label,
8
+ Page,
9
+ StackLayout,
10
+ )
11
+ from pyjs.tooling.nativescript import NativeScriptProject
12
+
13
+ from .calculator import calculate_result
14
+
15
+
16
+ @js
17
+ class CalculatorButtonListener:
18
+ def __init__(self, calculator: AndroidCalculator, kind: str, value: str):
19
+ self.calculator = calculator
20
+ self.kind = kind
21
+ self.value = value
22
+
23
+ def handleEvent(self, event: EventData):
24
+ self.calculator.press(self.kind, self.value)
25
+
26
+
27
+ @js
28
+ class AndroidCalculator:
29
+ def __init__(self):
30
+ self.display_value = "0"
31
+ self.stored_value = 0.0
32
+ self.pending_operator = ""
33
+ self.replace_display = True
34
+ self.error = False
35
+
36
+ self.display = Label()
37
+ self.display.id = "calculator-display"
38
+ self.display.testID = "calculator-display"
39
+ self.display.text = self.display_value
40
+ self.display.className = (
41
+ "mb-4 rounded-2xl bg-slate-950 p-5 text-right text-5xl "
42
+ "font-bold text-white"
43
+ )
44
+
45
+ def update_display(self):
46
+ self.display.text = self.display_value
47
+
48
+ def clear(self):
49
+ self.display_value = "0"
50
+ self.stored_value = 0.0
51
+ self.pending_operator = ""
52
+ self.replace_display = True
53
+ self.error = False
54
+ self.update_display()
55
+
56
+ def value(self) -> float:
57
+ if self.error:
58
+ return 0.0
59
+ return float(self.display_value)
60
+
61
+ def input_digit(self, digit: str):
62
+ if self.error:
63
+ self.clear()
64
+ if self.replace_display or self.display_value == "0":
65
+ self.display_value = digit
66
+ self.replace_display = False
67
+ else:
68
+ self.display_value += digit
69
+ self.update_display()
70
+
71
+ def input_decimal(self):
72
+ if self.error:
73
+ self.clear()
74
+ if self.replace_display:
75
+ self.display_value = "0."
76
+ self.replace_display = False
77
+ elif len(self.display_value.split(".")) == 1:
78
+ self.display_value += "."
79
+ self.update_display()
80
+
81
+ def choose_operator(self, operator: str):
82
+ if self.error:
83
+ return
84
+ if self.pending_operator != "" and not self.replace_display:
85
+ self.calculate()
86
+ if self.error:
87
+ return
88
+ self.stored_value = float(self.display_value)
89
+ self.pending_operator = operator
90
+ self.replace_display = True
91
+
92
+ def toggle_sign(self):
93
+ if self.error or self.display_value == "0" or self.display_value == "0.0":
94
+ return
95
+ self.display_value = str(0.0 - float(self.display_value))
96
+ self.update_display()
97
+
98
+ def percent(self):
99
+ if self.error:
100
+ return
101
+ self.display_value = str(float(self.display_value) / 100.0)
102
+ self.replace_display = True
103
+ self.update_display()
104
+
105
+ def calculate(self) -> float:
106
+ operator = self.pending_operator
107
+ if self.error or operator == "":
108
+ return self.value()
109
+ right = float(self.display_value)
110
+ if operator == "/" and right == 0.0:
111
+ self.display_value = "Error"
112
+ self.pending_operator = ""
113
+ self.replace_display = True
114
+ self.error = True
115
+ self.update_display()
116
+ return 0.0
117
+ result = calculate_result(self.stored_value, right, operator)
118
+ self.display_value = str(result)
119
+ self.stored_value = result
120
+ self.pending_operator = ""
121
+ self.replace_display = True
122
+ self.update_display()
123
+ return result
124
+
125
+ def press(self, kind: str, value: str):
126
+ if kind == "digit":
127
+ self.input_digit(value)
128
+ elif kind == "decimal":
129
+ self.input_decimal()
130
+ elif kind == "operator":
131
+ self.choose_operator(value)
132
+ elif kind == "equals":
133
+ self.calculate()
134
+ elif value == "clear":
135
+ self.clear()
136
+ elif value == "sign":
137
+ self.toggle_sign()
138
+ else:
139
+ self.percent()
140
+
141
+ def key(self, label: str, value: str, kind: str, row: int, column: int) -> Button:
142
+ key = Button()
143
+ key.id = "key-" + value
144
+ key.testID = "key-" + value
145
+ key.text = label
146
+ key.row = row
147
+ key.column = column
148
+ if kind == "operator" or kind == "equals":
149
+ key.className = (
150
+ "m-1 h-14 rounded-xl bg-amber-500 text-2xl font-bold text-white"
151
+ )
152
+ else:
153
+ key.className = (
154
+ "m-1 h-14 rounded-xl bg-slate-100 text-2xl font-semibold "
155
+ "text-slate-950"
156
+ )
157
+ listener = CalculatorButtonListener(self, kind, value)
158
+ key.on(Button.tapEvent, listener.handleEvent)
159
+ return key
160
+
161
+ def page(self) -> Page:
162
+ keypad = GridLayout()
163
+ keypad.rows = "auto,auto,auto,auto,auto"
164
+ keypad.columns = "*,*,*,*"
165
+ labels = [
166
+ ("AC", "clear", "clear"),
167
+ ("±", "sign", "action"),
168
+ ("%", "percent", "action"),
169
+ ("÷", "/", "operator"),
170
+ ("7", "7", "digit"),
171
+ ("8", "8", "digit"),
172
+ ("9", "9", "digit"),
173
+ ("×", "*", "operator"),
174
+ ("4", "4", "digit"),
175
+ ("5", "5", "digit"),
176
+ ("6", "6", "digit"),
177
+ ("−", "-", "operator"),
178
+ ("1", "1", "digit"),
179
+ ("2", "2", "digit"),
180
+ ("3", "3", "digit"),
181
+ ("+", "+", "operator"),
182
+ ("0", "0", "digit"),
183
+ (".", ".", "decimal"),
184
+ ("=", "=", "equals"),
185
+ ]
186
+ index = 0
187
+ for label, value, kind in labels:
188
+ keypad.addChild(self.key(label, value, kind, index // 4, index % 4))
189
+ index += 1
190
+
191
+ content = StackLayout()
192
+ content.className = "bg-slate-900 p-5"
193
+ content.verticalAlignment = "middle"
194
+ title = Label()
195
+ title.text = "PyJS Calculator"
196
+ title.className = "mb-4 text-center text-2xl font-bold text-sky-300"
197
+ content.addChild(title)
198
+ content.addChild(self.display)
199
+ content.addChild(keypad)
200
+
201
+ page = Page()
202
+ page.actionBarHidden = True
203
+ page.className = "bg-slate-900"
204
+ page.content = content
205
+ return page
206
+
207
+
208
+ @js
209
+ def create_android_page() -> Page:
210
+ return AndroidCalculator().page()
211
+
212
+
213
+ @js
214
+ def android_main():
215
+ Application.run(create_android_page)
216
+
217
+
218
+ def run_android():
219
+ project = NativeScriptProject(
220
+ android_main,
221
+ name="PyJS Calculator",
222
+ app_id="org.pyjs.calculator",
223
+ )
224
+ device = project.run_android()
225
+ print(f"PyJS Calculator is running on {device.identifier}.")
@@ -12,6 +12,17 @@ from pyjs.web.serviceworker import (
12
12
  from pyjs.web.ui import Widget
13
13
 
14
14
 
15
+ @js
16
+ def calculate_result(left: float, right: float, operator: str) -> float:
17
+ if operator == "+":
18
+ return left + right
19
+ if operator == "-":
20
+ return left - right
21
+ if operator == "*":
22
+ return left * right
23
+ return left / right
24
+
25
+
15
26
  @js
16
27
  async def install_app_shell(cache_name: str, assets: list[str]):
17
28
  cache = await caches.open(cache_name)
@@ -41,14 +52,12 @@ class InstallHandler:
41
52
  self.cache_name = cache_name
42
53
  self.assets = assets
43
54
 
44
- @js(include=True)
45
55
  def handleEvent(self, event: ExtendableEvent):
46
56
  event.waitUntil(install_app_shell(self.cache_name, self.assets))
47
57
 
48
58
 
49
59
  @js
50
60
  class ActivateHandler:
51
- @js(include=True)
52
61
  def handleEvent(self, event: ExtendableEvent):
53
62
  event.waitUntil(activate_app_shell())
54
63
 
@@ -58,7 +67,6 @@ class FetchHandler:
58
67
  def __init__(self, cache_name: str):
59
68
  self.cache_name = cache_name
60
69
 
61
- @js(include=True)
62
70
  def handleEvent(self, event: FetchEvent):
63
71
  if event.request.method == "GET":
64
72
  event.respondWith(cache_first(event.request, self.cache_name))
@@ -75,11 +83,11 @@ def service_worker_main():
75
83
 
76
84
  @nojs
77
85
  def service_worker_javascript(request):
78
- from pyjs.server import ServerResponse
86
+ from pyjs.server.dev import Response
79
87
  from pyjs.transpiler import bundle
80
88
 
81
89
  source, _ = bundle(service_worker_main, include_main=True, include_css=False)
82
- return ServerResponse(
90
+ return Response(
83
91
  source,
84
92
  content_type="application/javascript",
85
93
  headers={
@@ -96,7 +104,6 @@ class CalculatorKeyListener:
96
104
  self.kind = kind
97
105
  self.value = value
98
106
 
99
- @js(include=True)
100
107
  async def handleEvent(self, event: Event):
101
108
  self.app.press(self.kind, self.value)
102
109
  if self.kind == "equals" and not self.app.error:
@@ -334,15 +341,6 @@ class CalculatorApp(Widget):
334
341
  self.pending_operator = operator
335
342
  self.replace_display = True
336
343
 
337
- def calculate_result(self, left: float, right: float, operator: str) -> float:
338
- if operator == "+":
339
- return left + right
340
- if operator == "-":
341
- return left - right
342
- if operator == "*":
343
- return left * right
344
- return left / right
345
-
346
344
  def calculate(self) -> float:
347
345
  operator = self.pending_operator
348
346
  if self.error or operator == "":
@@ -354,7 +352,7 @@ class CalculatorApp(Widget):
354
352
  self.replace_display = True
355
353
  self.error = True
356
354
  return 0.0
357
- result = self.calculate_result(self.stored_value, right, operator)
355
+ result = calculate_result(self.stored_value, right, operator)
358
356
  self.display_value = str(result)
359
357
  self.stored_value = result
360
358
  self.pending_operator = ""
@@ -427,7 +425,7 @@ def save_result(request):
427
425
  if __name__ == "__main__":
428
426
  import os
429
427
 
430
- from pyjs.server import serve
428
+ from pyjs.server.dev import serve
431
429
 
432
430
  serve(
433
431
  "calculator",
@@ -0,0 +1,34 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+ from pyjs.testing.android import AndroidTestSession
5
+ from pyjs.tooling.android import AndroidSDK
6
+ from pyjs.tooling.nativescript import NativeScriptProject
7
+ from pyjs_examples.calculator.android import android_main
8
+
9
+ pytestmark = pytest.mark.pyjs_android
10
+
11
+
12
+ def test_calculator_runs_on_android(request, pyjs_artifacts_dir: Path):
13
+ android = AndroidSDK()
14
+ project = NativeScriptProject(
15
+ android_main,
16
+ name="PyJS Calculator",
17
+ app_id="org.pyjs.calculator",
18
+ android=android,
19
+ )
20
+ device = project.run_android(request.config.getoption("--pyjs-android-avd"))
21
+ session = AndroidTestSession(android, device, project.app_id)
22
+ artifacts = pyjs_artifacts_dir / "android"
23
+
24
+ with session.artifacts_on_failure(artifacts, "calculator"):
25
+ session.launch()
26
+ session.wait_for_element(test_id="calculator-display", text="0")
27
+ session.tap_element(test_id="key-1")
28
+ session.tap_element(test_id="key-+")
29
+ session.tap_element(test_id="key-2")
30
+ session.tap_element(test_id="key-=")
31
+ session.wait_for_element(test_id="calculator-display", text="3.0")
32
+ assert session.log_errors() == ""
33
+
34
+ session.capture_artifacts(artifacts, "calculator")
@@ -0,0 +1,26 @@
1
+ import pytest
2
+ from pyjs_examples.calculator.android import AndroidCalculator
3
+
4
+ pytestmark = pytest.mark.pyjs_python
5
+
6
+
7
+ def test_android_calculator_uses_the_same_arithmetic_behavior():
8
+ calculator = AndroidCalculator()
9
+
10
+ calculator.input_digit("1")
11
+ calculator.input_digit("2")
12
+ calculator.choose_operator("+")
13
+ calculator.input_digit("7")
14
+
15
+ assert calculator.calculate() == 19.0
16
+ assert calculator.display.text == "19.0"
17
+
18
+
19
+ def test_android_calculator_builds_a_complete_native_keypad():
20
+ calculator = AndroidCalculator()
21
+ page = calculator.page()
22
+
23
+ assert page.content.getViewById("calculator-display") is calculator.display
24
+ assert page.content.getViewById("key-clear") is not None
25
+ assert page.content.getViewById("key-+") is not None
26
+ assert page.content.getViewById("key-=") is not None