htag 2.0.0__tar.gz → 2.0.2__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 (46) hide show
  1. {htag-2.0.0 → htag-2.0.2}/PKG-INFO +38 -16
  2. {htag-2.0.0 → htag-2.0.2}/README.md +36 -14
  3. {htag-2.0.0 → htag-2.0.2}/htag/cli.py +1 -1
  4. {htag-2.0.0 → htag-2.0.2}/htag/client_js.py +48 -15
  5. {htag-2.0.0 → htag-2.0.2}/htag/core.py +38 -8
  6. {htag-2.0.0 → htag-2.0.2}/htag/runner.py +12 -6
  7. {htag-2.0.0 → htag-2.0.2}/htag/runners/chromeapp.py +2 -1
  8. {htag-2.0.0 → htag-2.0.2}/htag/web.py +2 -6
  9. {htag-2.0.0 → htag-2.0.2}/htag.egg-info/PKG-INFO +38 -16
  10. {htag-2.0.0 → htag-2.0.2}/htag.egg-info/SOURCES.txt +1 -0
  11. {htag-2.0.0 → htag-2.0.2}/pyproject.toml +3 -2
  12. htag-2.0.2/tests/test_deprecation_warnings.py +78 -0
  13. htag-2.0.2/tests/test_simple_events.py +190 -0
  14. htag-2.0.0/tests/test_simple_events.py +0 -83
  15. {htag-2.0.0 → htag-2.0.2}/htag/__init__.py +0 -0
  16. {htag-2.0.0 → htag-2.0.2}/htag/context.py +0 -0
  17. {htag-2.0.0 → htag-2.0.2}/htag/css.py +0 -0
  18. {htag-2.0.0 → htag-2.0.2}/htag/exceptions.py +0 -0
  19. {htag-2.0.0 → htag-2.0.2}/htag/logo.py +0 -0
  20. {htag-2.0.0 → htag-2.0.2}/htag/runners/__init__.py +0 -0
  21. {htag-2.0.0 → htag-2.0.2}/htag/runners/pyscript.py +0 -0
  22. {htag-2.0.0 → htag-2.0.2}/htag/server.py +0 -0
  23. {htag-2.0.0 → htag-2.0.2}/htag/tag.py +0 -0
  24. {htag-2.0.0 → htag-2.0.2}/htag/utils.py +0 -0
  25. {htag-2.0.0 → htag-2.0.2}/htag.egg-info/dependency_links.txt +0 -0
  26. {htag-2.0.0 → htag-2.0.2}/htag.egg-info/entry_points.txt +0 -0
  27. {htag-2.0.0 → htag-2.0.2}/htag.egg-info/requires.txt +0 -0
  28. {htag-2.0.0 → htag-2.0.2}/htag.egg-info/top_level.txt +0 -0
  29. {htag-2.0.0 → htag-2.0.2}/setup.cfg +0 -0
  30. {htag-2.0.0 → htag-2.0.2}/tests/test_cmd.py +0 -0
  31. {htag-2.0.0 → htag-2.0.2}/tests/test_core.py +0 -0
  32. {htag-2.0.0 → htag-2.0.2}/tests/test_fallback.py +0 -0
  33. {htag-2.0.0 → htag-2.0.2}/tests/test_lifecycle.py +0 -0
  34. {htag-2.0.0 → htag-2.0.2}/tests/test_memory.py +0 -0
  35. {htag-2.0.0 → htag-2.0.2}/tests/test_parano.py +0 -0
  36. {htag-2.0.0 → htag-2.0.2}/tests/test_reactivity_edge_cases.py +0 -0
  37. {htag-2.0.0 → htag-2.0.2}/tests/test_runner_pyscript.py +0 -0
  38. {htag-2.0.0 → htag-2.0.2}/tests/test_runners_reload.py +0 -0
  39. {htag-2.0.0 → htag-2.0.2}/tests/test_server.py +0 -0
  40. {htag-2.0.0 → htag-2.0.2}/tests/test_state_advanced.py +0 -0
  41. {htag-2.0.0 → htag-2.0.2}/tests/test_state_features.py +0 -0
  42. {htag-2.0.0 → htag-2.0.2}/tests/test_state_proxy.py +0 -0
  43. {htag-2.0.0 → htag-2.0.2}/tests/test_state_reactivity.py +0 -0
  44. {htag-2.0.0 → htag-2.0.2}/tests/test_tag_names.py +0 -0
  45. {htag-2.0.0 → htag-2.0.2}/tests/test_web_sessions.py +0 -0
  46. {htag-2.0.0 → htag-2.0.2}/tests/test_webapp_run.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htag
3
- Version: 2.0.0
4
- Summary: A modern, state-of-the-art Python GUI framework
3
+ Version: 2.0.2
4
+ Summary: Python3 GUI toolkit for building 'beautiful' applications for mobile, web, and desktop from a single codebase
5
5
  Author: manatlan
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/manatlan/htag
@@ -37,31 +37,46 @@ Requires-Dist: websockets>=16.0
37
37
  </a>
38
38
  </p>
39
39
 
40
- Here is a full rewrite of **htag 1.0.0**, using only antigravity and prompts/intentions.
40
+ **htag** is a Python3 GUI toolkit for building "beautiful" applications for mobile, web, and desktop from a single codebase
41
41
 
42
- It feels very good. Currently, it's the future of htag.
42
+ Here is a full rewrite of **htag 1.0.0**, using only antigravity and prompts/intentions. It's the future of **htag**.
43
43
 
44
44
  Currently, it works ...
45
45
 
46
- - For building desktop apps (ChromeApp on linux/windows)
46
+ - For building desktop apps (ChromeApp on linux/windows/mac, or simple WebApp)
47
47
  - For building web apps (WebApp as an asgi app (for starlette/fastapi/...))
48
48
  - For building SPA HTML Page (with the PyScript runner)
49
49
  - For building android apps ([it works](examples/app_android/README.md), **but need to improve**)
50
50
 
51
+ ## Major differences between v1 and v2
52
+
53
+ - A lot simpler & more reactive
54
+ - `Tag.App` is the main app component
55
+ - Full starlette compliant from the ground
56
+ - Websocket communication, if fails : fallback to HTTP SSE
57
+ - No more generic Runner
58
+ - State object for reactivity
59
+ - DX: errors are a lot better handled/viewable, hot-reload available
60
+ - Better events
61
+ - less boilerplate (kiss minded)
62
+ - Styles can be scoped by component
63
+ - ...
64
+
51
65
 
52
66
  ## Get Started
53
67
 
54
- Check the [Official Documentation](https://manatlan.github.io/htag/) for more information.
68
+ Check the [Official V2 Documentation](https://manatlan.github.io/htag/) for more information. [old v1 docs](https://manatlan.github.io/htag/v1/)
69
+
55
70
 
56
71
  ## Install
57
72
 
58
73
  ```bash
59
- uv add htag
74
+ uv add htag -U
60
75
  ```
61
76
 
62
77
  Or using pip:
63
78
  ```bash
64
- pip install htag
79
+ pip install -U htag
65
80
  ```
66
81
 
67
82
  Alternatively, you can run from source:
@@ -73,35 +88,42 @@ uv run examples/main3.py
73
88
 
74
89
  ### Skill
75
90
 
76
- With gemini-cli, claude-code, mistral-vibe (or others), you can use this [SKILL.md](.agent/skills/htag-development/SKILL.md) to create a htag application.
91
+ With agentic llm, you can use this [SKILL.md](.agent/skills/htag-development/SKILL.md) to create an htag application.
77
92
 
78
93
 
79
94
  ## Antigravity resumes :
80
95
 
81
96
  htag is a Python library for building web applications using HTML, CSS, and JavaScript.
82
97
 
83
- ### Key Resiliency Features Added
98
+ ### New Features
84
99
  * **Zero-Config Hot-Reload**: Passing `reload=True` to any runner (e.g. `ChromeApp(App).run(reload=True)`) automatically watches for Python file changes, seamlessly restarts the backend, and gracefully refreshes the frontend without losing your browser window session.
85
100
  * **F5/Reload Robustness**: Refreshing the browser no longer kills the Python backend; the session reconstructs cleanly.
86
101
  * **HTTP Fallback (SSE + POST)**: If WebSockets are blocked (e.g. strict proxies) or fail to connect, the client seamlessly falls back to HTTP POST for events and Server-Sent Events (SSE) for receiving UI updates.
87
102
  * **Production Debug Mode**: Easily disable error reporting in the client by setting `debug=False` on the runner (e.g. `WebApp(App, debug=False).app`), preventing internal stacktraces from leaking to users.
88
103
  * **Parano Mode (Payload Obfuscation)**: By initializing `WebApp(App, parano=True)`, all data exchanged between the frontend and backend is automatically obfuscated using a dynamic XOR cipher and Base64 wrapping, making network traffic unreadable to MITM proxies.
89
-
90
- ### New API Features
91
104
  * **`.root`, `.parent`, and `.childs` properties**: Every `GTag` exposes its position in the component tree. `.root` references the main `Tag` instance, `.parent` references the direct parent component, and `.childs` is a list of its children. This allows components to easily navigate the DOM tree and trigger app-level actions.
92
105
  * **Declarative UI with Context Managers (`with`)**: You can now build component trees visually using `with` blocks (e.g., `with Tag.div(): Tag.h1("Hello")`), removing the need for `self <= ...` boilerplate.
93
- * **Dual Attribute Access (Dictionary Style)**: In addition to `self._class = "foo"`, you can now use `self["class"] = "foo"`. This is the preferred way to handle attributes with dashes (e.g., `self["data-id"] = 123`).
94
- * **Automatic Attribute Normalization**: HTML attributes are normalized to use dashes internally. Setting `self._data_id = "123"` is strictly equivalent to setting `self["data-id"] = "123"`.
106
+ * **Modern Attribute Access (Dictionary Style)**: In addition to `Tag.div(_class="foo")` in constructors, you should now use `self["class"] = "foo"` for dynamic property management. This is the preferred way to handle all attributes, including those with dashes (e.g., `self["data-id"] = 123`).
95
107
  * **Reactive State Management (`State`)**: Introducing `State(value)` for automatic UI reactivity. `State` acts as a transparent **Proxy**: you can use comparison/arithmetic operators directly (`if self.count > 0: ...`), mutate nested structures (including `lists`, `dicts`, `sets`, and `tuples`), and delegate attribute assignments seamlessly.
96
108
  * **Iterative & Attribute Reactivity**: Iterating over a `State` now yields proxies, meaning mutations like `for item in self.list: item.active = True` trigger re-renders. Attribute assignments (e.g. `self.user.name = "Bob"`) are automatically delegated to the underlying object and notified.
97
- * **Reactive & Boolean Attributes**: Attributes like `_class`, `_style`, or `_disabled` now support lambdas for dynamic updates. Boolean attributes (e.g. `_disabled=True`) are correctly rendered as key-only or omitted.
109
+ * **Reactive & Boolean Attributes**: Attributes like `_class`, `_style`, or `_disabled` in constructors now support lambdas for dynamic updates. Boolean attributes (e.g. `_disabled=True`) are correctly rendered as key-only or omitted.
98
110
  * **Simplified Removal (`.remove()`)**: To remove a component from its parent, simply call `self.remove()` without arguments.
99
111
  * **Rapid Content Replacement (`.text`)**: Use the `.text` property on any tag to quickly replace its inner text content without needing to manually clear its children first.
100
112
  * **Recursive Statics & JS**: Components created dynamically (via lambdas) now have their `statics` (CSS) and `call_js` commands correctly collected and sent to the client.
101
113
  * **Scoped Styles (`styles`)**: Define a `styles` class attribute on any component to get automatically scoped CSS. The framework prefixes every rule with `.htag-ClassName`, handles `@media`, `@keyframes`, pseudo-selectors, and multi-selectors.
102
114
  * **CSS Class Helpers**: `add_class()`, `remove_class()`, `toggle_class()`, and `has_class()` for convenient class manipulation without manual string handling.
103
- * **Simple Events & HashChange**: Support for passing primitive values or custom objects from JS (via `tag["onclick"] = func` or `tag._onclick = func`). Includes built-in support for `_onhashchange`.
115
+ * **Simple Events & HashChange**: Support for passing primitive values or custom objects from JS (via `tag["onclick"] = func`). Includes built-in support for `onhashchange`.
104
116
  * **Fragments (`Tag()`)**: Create virtual components that group children without adding a wrapper tag to the DOM.
105
117
  * **Advanced Tag Search (`.find_tag()`)**: Effortlessly locate any component in the tree by its internal htag ID or its manually assigned HTML `id`.
106
118
  * **Custom ID Resilience**: Manual HTML `id` attributes are now supported without breaking htag's reactive partial updates, thanks to an automatic `data-htag-id` fallback mechanism.
107
119
  * **Automatic Attribute Assignment**: Non-prefixed keyword arguments passed during component instantiation are automatically assigned as instance attributes, simplifying data passing to custom components.
120
+ * **Unified Form Handling**: When a `Tag.form` is submitted, all input fields are automatically collected into a dictionary and passed as `event.value`. You can conveniently access fields using square brackets on the event object (e.g., `e["fieldname"]`).
121
+
122
+
123
+ ## History
124
+
125
+ At the beginning, there was [guy](https://github.com/manatlan/guy), which was/is the same concept as [python-eel](https://github.com/ChrisKnott/Eel), but more advanced.
126
+ One day, I've discovered [remi](https://github.com/rawpython/remi), and asked my self, if it could be done in a *guy way*. The POC was very good, so I released
127
+ a version of it, named [gtag](https://github.com/manatlan/gtag). It worked well despite some drawbacks, but was too difficult to maintain. So I decided to rewrite all
128
+ from scratch, while staying away from *guy* (to separate, *rendering* and *runners*)... and [htag](https://github.com/manatlan/htag/tree/v1-legacy) was born. The codebase is very short, concepts are better implemented, and it's very easy to maintain. And now (2026) [htag v2](https://github.com/manatlan/htag) is here (a full rewrite of v1 with antigravity) !
129
+
@@ -16,31 +16,46 @@
16
16
  </a>
17
17
  </p>
18
18
 
19
- Here is a full rewrite of **htag 1.0.0**, using only antigravity and prompts/intentions.
19
+ **htag** is a Python3 GUI toolkit for building "beautiful" applications for mobile, web, and desktop from a single codebase
20
20
 
21
- It feels very good. Currently, it's the future of htag.
21
+ Here is a full rewrite of **htag 1.0.0**, using only antigravity and prompts/intentions. It's the future of **htag**.
22
22
 
23
23
  Currently, it works ...
24
24
 
25
- - For building desktop apps (ChromeApp on linux/windows)
25
+ - For building desktop apps (ChromeApp on linux/windows/mac, or simple WebApp)
26
26
  - For building web apps (WebApp as an asgi app (for starlette/fastapi/...))
27
27
  - For building SPA HTML Page (with the PyScript runner)
28
28
  - For building android apps ([it works](examples/app_android/README.md), **but need to improve**)
29
29
 
30
+ ## Major differences between v1 and v2
31
+
32
+ - A lot simpler & more reactive
33
+ - `Tag.App` is the main app component
34
+ - Full starlette compliant from the ground
35
+ - Websocket communication, if fails : fallback to HTTP SSE
36
+ - No more generic Runner
37
+ - State object for reactivity
38
+ - DX: errors are a lot better handled/viewable, hot-reload available
39
+ - Better events
40
+ - less boilerplate (kiss minded)
41
+ - Styles can be scoped by component
42
+ - ...
43
+
30
44
 
31
45
  ## Get Started
32
46
 
33
- Check the [Official Documentation](https://manatlan.github.io/htag/) for more information.
47
+ Check the [Official V2 Documentation](https://manatlan.github.io/htag/) for more information. [old v1 docs](https://manatlan.github.io/htag/v1/)
48
+
34
49
 
35
50
  ## Install
36
51
 
37
52
  ```bash
38
- uv add htag
53
+ uv add htag -U
39
54
  ```
40
55
 
41
56
  Or using pip:
42
57
  ```bash
43
- pip install htag
58
+ pip install -U htag
44
59
  ```
45
60
 
46
61
  Alternatively, you can run from source:
@@ -52,35 +67,42 @@ uv run examples/main3.py
52
67
 
53
68
  ### Skill
54
69
 
55
- With gemini-cli, claude-code, mistral-vibe (or others), you can use this [SKILL.md](.agent/skills/htag-development/SKILL.md) to create a htag application.
70
+ With agentic llm, you can use this [SKILL.md](.agent/skills/htag-development/SKILL.md) to create an htag application.
56
71
 
57
72
 
58
73
  ## Antigravity resumes :
59
74
 
60
75
  htag is a Python library for building web applications using HTML, CSS, and JavaScript.
61
76
 
62
- ### Key Resiliency Features Added
77
+ ### New Features
63
78
  * **Zero-Config Hot-Reload**: Passing `reload=True` to any runner (e.g. `ChromeApp(App).run(reload=True)`) automatically watches for Python file changes, seamlessly restarts the backend, and gracefully refreshes the frontend without losing your browser window session.
64
79
  * **F5/Reload Robustness**: Refreshing the browser no longer kills the Python backend; the session reconstructs cleanly.
65
80
  * **HTTP Fallback (SSE + POST)**: If WebSockets are blocked (e.g. strict proxies) or fail to connect, the client seamlessly falls back to HTTP POST for events and Server-Sent Events (SSE) for receiving UI updates.
66
81
  * **Production Debug Mode**: Easily disable error reporting in the client by setting `debug=False` on the runner (e.g. `WebApp(App, debug=False).app`), preventing internal stacktraces from leaking to users.
67
82
  * **Parano Mode (Payload Obfuscation)**: By initializing `WebApp(App, parano=True)`, all data exchanged between the frontend and backend is automatically obfuscated using a dynamic XOR cipher and Base64 wrapping, making network traffic unreadable to MITM proxies.
68
-
69
- ### New API Features
70
83
  * **`.root`, `.parent`, and `.childs` properties**: Every `GTag` exposes its position in the component tree. `.root` references the main `Tag` instance, `.parent` references the direct parent component, and `.childs` is a list of its children. This allows components to easily navigate the DOM tree and trigger app-level actions.
71
84
  * **Declarative UI with Context Managers (`with`)**: You can now build component trees visually using `with` blocks (e.g., `with Tag.div(): Tag.h1("Hello")`), removing the need for `self <= ...` boilerplate.
72
- * **Dual Attribute Access (Dictionary Style)**: In addition to `self._class = "foo"`, you can now use `self["class"] = "foo"`. This is the preferred way to handle attributes with dashes (e.g., `self["data-id"] = 123`).
73
- * **Automatic Attribute Normalization**: HTML attributes are normalized to use dashes internally. Setting `self._data_id = "123"` is strictly equivalent to setting `self["data-id"] = "123"`.
85
+ * **Modern Attribute Access (Dictionary Style)**: In addition to `Tag.div(_class="foo")` in constructors, you should now use `self["class"] = "foo"` for dynamic property management. This is the preferred way to handle all attributes, including those with dashes (e.g., `self["data-id"] = 123`).
74
86
  * **Reactive State Management (`State`)**: Introducing `State(value)` for automatic UI reactivity. `State` acts as a transparent **Proxy**: you can use comparison/arithmetic operators directly (`if self.count > 0: ...`), mutate nested structures (including `lists`, `dicts`, `sets`, and `tuples`), and delegate attribute assignments seamlessly.
75
87
  * **Iterative & Attribute Reactivity**: Iterating over a `State` now yields proxies, meaning mutations like `for item in self.list: item.active = True` trigger re-renders. Attribute assignments (e.g. `self.user.name = "Bob"`) are automatically delegated to the underlying object and notified.
76
- * **Reactive & Boolean Attributes**: Attributes like `_class`, `_style`, or `_disabled` now support lambdas for dynamic updates. Boolean attributes (e.g. `_disabled=True`) are correctly rendered as key-only or omitted.
88
+ * **Reactive & Boolean Attributes**: Attributes like `_class`, `_style`, or `_disabled` in constructors now support lambdas for dynamic updates. Boolean attributes (e.g. `_disabled=True`) are correctly rendered as key-only or omitted.
77
89
  * **Simplified Removal (`.remove()`)**: To remove a component from its parent, simply call `self.remove()` without arguments.
78
90
  * **Rapid Content Replacement (`.text`)**: Use the `.text` property on any tag to quickly replace its inner text content without needing to manually clear its children first.
79
91
  * **Recursive Statics & JS**: Components created dynamically (via lambdas) now have their `statics` (CSS) and `call_js` commands correctly collected and sent to the client.
80
92
  * **Scoped Styles (`styles`)**: Define a `styles` class attribute on any component to get automatically scoped CSS. The framework prefixes every rule with `.htag-ClassName`, handles `@media`, `@keyframes`, pseudo-selectors, and multi-selectors.
81
93
  * **CSS Class Helpers**: `add_class()`, `remove_class()`, `toggle_class()`, and `has_class()` for convenient class manipulation without manual string handling.
82
- * **Simple Events & HashChange**: Support for passing primitive values or custom objects from JS (via `tag["onclick"] = func` or `tag._onclick = func`). Includes built-in support for `_onhashchange`.
94
+ * **Simple Events & HashChange**: Support for passing primitive values or custom objects from JS (via `tag["onclick"] = func`). Includes built-in support for `onhashchange`.
83
95
  * **Fragments (`Tag()`)**: Create virtual components that group children without adding a wrapper tag to the DOM.
84
96
  * **Advanced Tag Search (`.find_tag()`)**: Effortlessly locate any component in the tree by its internal htag ID or its manually assigned HTML `id`.
85
97
  * **Custom ID Resilience**: Manual HTML `id` attributes are now supported without breaking htag's reactive partial updates, thanks to an automatic `data-htag-id` fallback mechanism.
86
98
  * **Automatic Attribute Assignment**: Non-prefixed keyword arguments passed during component instantiation are automatically assigned as instance attributes, simplifying data passing to custom components.
99
+ * **Unified Form Handling**: When a `Tag.form` is submitted, all input fields are automatically collected into a dictionary and passed as `event.value`. You can conveniently access fields using square brackets on the event object (e.g., `e["fieldname"]`).
100
+
101
+
102
+ ## History
103
+
104
+ At the beginning, there was [guy](https://github.com/manatlan/guy), which was/is the same concept as [python-eel](https://github.com/ChrisKnott/Eel), but more advanced.
105
+ One day, I've discovered [remi](https://github.com/rawpython/remi), and asked my self, if it could be done in a *guy way*. The POC was very good, so I released
106
+ a version of it, named [gtag](https://github.com/manatlan/gtag). It worked well despite some drawbacks, but was too difficult to maintain. So I decided to rewrite all
107
+ from scratch, while staying away from *guy* (to separate, *rendering* and *runners*)... and [htag](https://github.com/manatlan/htag/tree/v1-legacy) was born. The codebase is very short, concepts are better implemented, and it's very easy to maintain. And now (2026) [htag v2](https://github.com/manatlan/htag) is here (a full rewrite of v1 with antigravity) !
108
+
@@ -176,7 +176,7 @@ version = 1.0
176
176
  source.dir = .
177
177
  source.include_exts = py,png,jpg,jpeg,svg,js,css,html
178
178
 
179
- requirements = android,htag,starlette,uvicorn,websockets,anyio,typing_extensions,click,httpx,h11
179
+ requirements = android,htag>=2.0.0,starlette,uvicorn,websockets,anyio,typing_extensions,click,httpx,h11
180
180
  orientation = {orientation}
181
181
  fullscreen = {fullscreen}
182
182
  android.archs = {android_archs}
@@ -143,6 +143,7 @@ function init_ws() {
143
143
 
144
144
  function handle_payload(data) {
145
145
  if(data.action == "update") {
146
+ console.log("htag: processing payload updates:", Object.keys(data.updates || {}));
146
147
  // Apply partial DOM updates received from the server
147
148
  for(var id in data.updates) {
148
149
  var el = document.getElementById(id) || document.querySelector('[data-htag-id="' + id + '"]');
@@ -153,21 +154,46 @@ function handle_payload(data) {
153
154
  if(_error_overlay && _error_overlay.parentNode !== document.body) {
154
155
  document.body.appendChild(_error_overlay);
155
156
  }
156
- // Execute any JavaScript calls emitted by the Python tags
157
- if(data.js) {
158
- for(var i=0; i<data.js.length; i++) eval(data.js[i]);
159
- }
160
- // Inject new css/js statics if they haven't been loaded yet
157
+ // 1. Inject new css/js statics if they haven't been loaded yet (BEFORE JS calls)
161
158
  if(data.statics) {
162
159
  data.statics.forEach(s => {
163
- var div = document.createElement('div');
164
- div.innerHTML = s.trim();
165
- var node = div.firstChild;
166
- if (node && (node.tagName === "STYLE" || node.tagName === "LINK")) {
167
- document.head.appendChild(node);
160
+ try {
161
+ var div = document.createElement('div');
162
+ div.innerHTML = s.trim();
163
+ var node = div.firstChild;
164
+ if (!node) return;
165
+
166
+ if (node.tagName === "STYLE" || node.tagName === "LINK") {
167
+ document.head.appendChild(node);
168
+ } else if (node.tagName === "SCRIPT") {
169
+ var script = document.createElement('script');
170
+ script.async = false; // Force sequential execution for multiple dynamic scripts
171
+ for (var i = 0; i < node.attributes.length; i++) {
172
+ var attr = node.attributes[i];
173
+ script.setAttribute(attr.name, attr.value);
174
+ }
175
+ if (node.textContent) script.textContent = node.textContent;
176
+ document.head.appendChild(script);
177
+ }
178
+ } catch(e) {
179
+ console.error("htag: static injection error", s, e);
168
180
  }
169
181
  });
170
182
  }
183
+
184
+ // 2. Execute any JavaScript calls emitted by the Python tags
185
+ if(data.js) {
186
+ for(var i=0; i<data.js.length; i++) {
187
+ try {
188
+ eval(data.js[i]);
189
+ } catch(e) {
190
+ console.error("htag: eval error for", data.js[i], e);
191
+ if(_error_overlay && typeof _error_overlay.show === 'function') {
192
+ _error_overlay.show("JS Eval Error", e.message + "\\nSource: " + data.js[i]);
193
+ }
194
+ }
195
+ }
196
+ }
171
197
  // Resolve promise if a result is returned for a callback
172
198
  if(data.callback_id && window._htag_callbacks[data.callback_id]) {
173
199
  window._htag_callbacks[data.callback_id](data.result);
@@ -262,11 +288,19 @@ function htag_event(id, event_name, event) {
262
288
 
263
289
  if (event instanceof Event) {
264
290
  // Standard DOM Event
265
- if (event.target) {
266
- if (event.target.type === 'checkbox') {
267
- data.value = event.target.checked;
291
+ var target = event.target;
292
+ if (target && target.tagName) { // Ensure target is a DOM element
293
+ // Check if the event source is a form or inside a form
294
+ var form = (target.tagName === 'FORM') ? target : target.closest('form');
295
+ if (form && event_name === 'submit') {
296
+ // Collect all form data into value attribute (standard htag v2 pattern)
297
+ var formData = new FormData(form);
298
+ data.value = {};
299
+ formData.forEach((v, k) => { data.value[k] = v; });
300
+ } else if (target.type === 'checkbox') {
301
+ data.value = target.checked;
268
302
  } else {
269
- data.value = event.target.value;
303
+ data.value = target.value;
270
304
  }
271
305
  }
272
306
  data.key = event.key;
@@ -285,7 +319,6 @@ function htag_event(id, event_name, event) {
285
319
  }
286
320
 
287
321
  var payload = {id: id, event: event_name, data: data};
288
-
289
322
  window.htag_transport(payload);
290
323
 
291
324
  return new Promise(resolve => {
@@ -562,15 +562,27 @@ class GTag: # aka "Generic Tag"
562
562
  "parent",
563
563
  "tag",
564
564
  "id",
565
+ "_reload",
566
+ "_browser_cleanup",
565
567
  ):
566
568
  super().__setattr__(name, value)
567
569
  elif name.startswith("_on") and (callable(value) or isinstance(value, str)):
568
570
  # Event (e.g., self._onclick = my_callback or self._onclick = "alert(1)")
571
+ logger.warning(
572
+ "DEPRECATION: Setting events via underscore prefix ('%s') is deprecated. Use 'self[\"%s\"] = ...' instead.",
573
+ name,
574
+ name[1:],
575
+ )
569
576
  with self.__lock:
570
577
  self.__events[name[3:]] = value
571
578
  self.__dirty = True
572
579
  elif name.startswith("_"):
573
580
  # HTML attribute (e.g., self._class = "foo")
581
+ logger.warning(
582
+ "DEPRECATION: Setting HTML attributes via underscore prefix ('%s') is deprecated. Use 'self[\"%s\"] = ...' instead.",
583
+ name,
584
+ name[1:].replace("_", "-"),
585
+ )
574
586
  attr_name = name[1:].replace("_", "-")
575
587
  with self.__lock:
576
588
  self.__attrs[attr_name] = value
@@ -580,15 +592,33 @@ class GTag: # aka "Generic Tag"
580
592
  super().__setattr__(name, value)
581
593
 
582
594
  def __getattr__(self, name: str) -> Any:
595
+ if name in ("_reload", "_browser_cleanup"):
596
+ return super().__getattribute__(name)
583
597
  if name.startswith("_") and "__" not in name:
584
- try:
585
- # Use super().__getattribute__ to avoid recursion loop with __getattr__
586
- attrs = super().__getattribute__("_GTag__attrs")
587
- attr_name = name[1:].replace("_", "-")
588
- if attr_name in attrs:
589
- return attrs[attr_name]
590
- except AttributeError:
591
- pass
598
+ if name.startswith("_on"):
599
+ event_name = name[3:]
600
+ events = super().__getattribute__("_GTag__events")
601
+ if event_name in events:
602
+ logger.warning(
603
+ "DEPRECATION: Accessing events via underscore prefix ('%s') is deprecated. Use 'self[\"%s\"]' instead.",
604
+ name,
605
+ name[1:],
606
+ )
607
+ return events[event_name]
608
+ else:
609
+ try:
610
+ # Use super().__getattribute__ to avoid recursion loop with __getattr__
611
+ attrs = super().__getattribute__("_GTag__attrs")
612
+ attr_name = name[1:].replace("_", "-")
613
+ if attr_name in attrs:
614
+ logger.warning(
615
+ "DEPRECATION: Accessing HTML attributes via underscore prefix ('%s') is deprecated. Use 'self[\"%s\"]' instead.",
616
+ name,
617
+ attr_name,
618
+ )
619
+ return attrs[attr_name]
620
+ except AttributeError:
621
+ pass
592
622
  return super().__getattribute__(name)
593
623
 
594
624
  def __getitem__(self, name: str) -> Any:
@@ -34,13 +34,18 @@ class Event:
34
34
  self.target = target
35
35
  self.id: str = msg.get("id", "")
36
36
  self.name: str = msg.get("event", "")
37
+ # The primary data payload (htag v2 pattern)
38
+ self.value = msg.get("data")
37
39
  # Flat access to msg['data'] (e.g., e.value, e.x, etc.)
38
- data = msg.get("data", {})
39
- if isinstance(data, dict):
40
- for k, v in data.items():
41
- setattr(self, k, v)
42
- else:
43
- self.value = data
40
+ data = self.value if isinstance(self.value, dict) else {}
41
+ for k, v in data.items():
42
+ setattr(self, k, v)
43
+
44
+ def __getitem__(self, name: str) -> Any:
45
+ val = getattr(self, "value", None)
46
+ if isinstance(val, dict) and name in val:
47
+ return val[name]
48
+ return getattr(self, name, None)
44
49
 
45
50
  def __getattr__(self, name: str) -> Any:
46
51
  return None
@@ -270,6 +275,7 @@ class AppRunner(BaseApp):
270
275
  self._walk_tree(tag, visitor)
271
276
 
272
277
  async def handle_event(self, msg: dict[str, Any], ws: WebSocket | None) -> None:
278
+ print(f"SERVERSIDE: handle_event {msg}")
273
279
  tag_id: str | None = msg.get("id")
274
280
  event_name: str | None = msg.get("event")
275
281
 
@@ -203,7 +203,8 @@ class ChromeApp:
203
203
  def on_inst(inst: App) -> None:
204
204
  inst.exit_on_disconnect = True
205
205
  if self._cleanup_func:
206
- setattr(inst, "_browser_cleanup", self._cleanup_func)
206
+ # Use object.__setattr__ to bypass GTag.__setattr__ and avoid deprecation warning
207
+ object.__setattr__(inst, "_browser_cleanup", self._cleanup_func)
207
208
 
208
209
  if not inspect.isclass(self.app):
209
210
  self.app.exit_on_disconnect = True
@@ -93,14 +93,10 @@ class WebApp:
93
93
  token = current_request.set(request_or_ws)
94
94
  try:
95
95
  if inspect.isclass(self.tag_entity):
96
- if issubclass(self.tag_entity, BaseApp):
97
- self.instances[sid] = self.tag_entity()
98
- else:
99
- # Wrap plain GTag class in an App runner
100
- self.instances[sid] = AppRunner(self.tag_entity)
96
+ self.instances[sid] = self.tag_entity()
101
97
  logger.info("Created new session instance for sid: %s", sid)
102
98
  else:
103
- # tag_entity is an App instance
99
+ # tag_entity is an App instance (shared)
104
100
  self.instances[sid] = self.tag_entity # type: ignore
105
101
  logger.info(
106
102
  "Using shared instance for session sid: %s", sid
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htag
3
- Version: 2.0.0
4
- Summary: A modern, state-of-the-art Python GUI framework
3
+ Version: 2.0.2
4
+ Summary: Python3 GUI toolkit for building 'beautiful' applications for mobile, web, and desktop from a single codebase
5
5
  Author: manatlan
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/manatlan/htag
@@ -37,31 +37,46 @@ Requires-Dist: websockets>=16.0
37
37
  </a>
38
38
  </p>
39
39
 
40
- Here is a full rewrite of **htag 1.0.0**, using only antigravity and prompts/intentions.
40
+ **htag** is a Python3 GUI toolkit for building "beautiful" applications for mobile, web, and desktop from a single codebase
41
41
 
42
- It feels very good. Currently, it's the future of htag.
42
+ Here is a full rewrite of **htag 1.0.0**, using only antigravity and prompts/intentions. It's the future of **htag**.
43
43
 
44
44
  Currently, it works ...
45
45
 
46
- - For building desktop apps (ChromeApp on linux/windows)
46
+ - For building desktop apps (ChromeApp on linux/windows/mac, or simple WebApp)
47
47
  - For building web apps (WebApp as an asgi app (for starlette/fastapi/...))
48
48
  - For building SPA HTML Page (with the PyScript runner)
49
49
  - For building android apps ([it works](examples/app_android/README.md), **but need to improve**)
50
50
 
51
+ ## Major differences between v1 and v2
52
+
53
+ - A lot simpler & more reactive
54
+ - `Tag.App` is the main app component
55
+ - Full starlette compliant from the ground
56
+ - Websocket communication, if fails : fallback to HTTP SSE
57
+ - No more generic Runner
58
+ - State object for reactivity
59
+ - DX: errors are a lot better handled/viewable, hot-reload available
60
+ - Better events
61
+ - less boilerplate (kiss minded)
62
+ - Styles can be scoped by component
63
+ - ...
64
+
51
65
 
52
66
  ## Get Started
53
67
 
54
- Check the [Official Documentation](https://manatlan.github.io/htag/) for more information.
68
+ Check the [Official V2 Documentation](https://manatlan.github.io/htag/) for more information. [old v1 docs](https://manatlan.github.io/htag/v1/)
69
+
55
70
 
56
71
  ## Install
57
72
 
58
73
  ```bash
59
- uv add htag
74
+ uv add htag -U
60
75
  ```
61
76
 
62
77
  Or using pip:
63
78
  ```bash
64
- pip install htag
79
+ pip install -U htag
65
80
  ```
66
81
 
67
82
  Alternatively, you can run from source:
@@ -73,35 +88,42 @@ uv run examples/main3.py
73
88
 
74
89
  ### Skill
75
90
 
76
- With gemini-cli, claude-code, mistral-vibe (or others), you can use this [SKILL.md](.agent/skills/htag-development/SKILL.md) to create a htag application.
91
+ With agentic llm, you can use this [SKILL.md](.agent/skills/htag-development/SKILL.md) to create an htag application.
77
92
 
78
93
 
79
94
  ## Antigravity resumes :
80
95
 
81
96
  htag is a Python library for building web applications using HTML, CSS, and JavaScript.
82
97
 
83
- ### Key Resiliency Features Added
98
+ ### New Features
84
99
  * **Zero-Config Hot-Reload**: Passing `reload=True` to any runner (e.g. `ChromeApp(App).run(reload=True)`) automatically watches for Python file changes, seamlessly restarts the backend, and gracefully refreshes the frontend without losing your browser window session.
85
100
  * **F5/Reload Robustness**: Refreshing the browser no longer kills the Python backend; the session reconstructs cleanly.
86
101
  * **HTTP Fallback (SSE + POST)**: If WebSockets are blocked (e.g. strict proxies) or fail to connect, the client seamlessly falls back to HTTP POST for events and Server-Sent Events (SSE) for receiving UI updates.
87
102
  * **Production Debug Mode**: Easily disable error reporting in the client by setting `debug=False` on the runner (e.g. `WebApp(App, debug=False).app`), preventing internal stacktraces from leaking to users.
88
103
  * **Parano Mode (Payload Obfuscation)**: By initializing `WebApp(App, parano=True)`, all data exchanged between the frontend and backend is automatically obfuscated using a dynamic XOR cipher and Base64 wrapping, making network traffic unreadable to MITM proxies.
89
-
90
- ### New API Features
91
104
  * **`.root`, `.parent`, and `.childs` properties**: Every `GTag` exposes its position in the component tree. `.root` references the main `Tag` instance, `.parent` references the direct parent component, and `.childs` is a list of its children. This allows components to easily navigate the DOM tree and trigger app-level actions.
92
105
  * **Declarative UI with Context Managers (`with`)**: You can now build component trees visually using `with` blocks (e.g., `with Tag.div(): Tag.h1("Hello")`), removing the need for `self <= ...` boilerplate.
93
- * **Dual Attribute Access (Dictionary Style)**: In addition to `self._class = "foo"`, you can now use `self["class"] = "foo"`. This is the preferred way to handle attributes with dashes (e.g., `self["data-id"] = 123`).
94
- * **Automatic Attribute Normalization**: HTML attributes are normalized to use dashes internally. Setting `self._data_id = "123"` is strictly equivalent to setting `self["data-id"] = "123"`.
106
+ * **Modern Attribute Access (Dictionary Style)**: In addition to `Tag.div(_class="foo")` in constructors, you should now use `self["class"] = "foo"` for dynamic property management. This is the preferred way to handle all attributes, including those with dashes (e.g., `self["data-id"] = 123`).
95
107
  * **Reactive State Management (`State`)**: Introducing `State(value)` for automatic UI reactivity. `State` acts as a transparent **Proxy**: you can use comparison/arithmetic operators directly (`if self.count > 0: ...`), mutate nested structures (including `lists`, `dicts`, `sets`, and `tuples`), and delegate attribute assignments seamlessly.
96
108
  * **Iterative & Attribute Reactivity**: Iterating over a `State` now yields proxies, meaning mutations like `for item in self.list: item.active = True` trigger re-renders. Attribute assignments (e.g. `self.user.name = "Bob"`) are automatically delegated to the underlying object and notified.
97
- * **Reactive & Boolean Attributes**: Attributes like `_class`, `_style`, or `_disabled` now support lambdas for dynamic updates. Boolean attributes (e.g. `_disabled=True`) are correctly rendered as key-only or omitted.
109
+ * **Reactive & Boolean Attributes**: Attributes like `_class`, `_style`, or `_disabled` in constructors now support lambdas for dynamic updates. Boolean attributes (e.g. `_disabled=True`) are correctly rendered as key-only or omitted.
98
110
  * **Simplified Removal (`.remove()`)**: To remove a component from its parent, simply call `self.remove()` without arguments.
99
111
  * **Rapid Content Replacement (`.text`)**: Use the `.text` property on any tag to quickly replace its inner text content without needing to manually clear its children first.
100
112
  * **Recursive Statics & JS**: Components created dynamically (via lambdas) now have their `statics` (CSS) and `call_js` commands correctly collected and sent to the client.
101
113
  * **Scoped Styles (`styles`)**: Define a `styles` class attribute on any component to get automatically scoped CSS. The framework prefixes every rule with `.htag-ClassName`, handles `@media`, `@keyframes`, pseudo-selectors, and multi-selectors.
102
114
  * **CSS Class Helpers**: `add_class()`, `remove_class()`, `toggle_class()`, and `has_class()` for convenient class manipulation without manual string handling.
103
- * **Simple Events & HashChange**: Support for passing primitive values or custom objects from JS (via `tag["onclick"] = func` or `tag._onclick = func`). Includes built-in support for `_onhashchange`.
115
+ * **Simple Events & HashChange**: Support for passing primitive values or custom objects from JS (via `tag["onclick"] = func`). Includes built-in support for `onhashchange`.
104
116
  * **Fragments (`Tag()`)**: Create virtual components that group children without adding a wrapper tag to the DOM.
105
117
  * **Advanced Tag Search (`.find_tag()`)**: Effortlessly locate any component in the tree by its internal htag ID or its manually assigned HTML `id`.
106
118
  * **Custom ID Resilience**: Manual HTML `id` attributes are now supported without breaking htag's reactive partial updates, thanks to an automatic `data-htag-id` fallback mechanism.
107
119
  * **Automatic Attribute Assignment**: Non-prefixed keyword arguments passed during component instantiation are automatically assigned as instance attributes, simplifying data passing to custom components.
120
+ * **Unified Form Handling**: When a `Tag.form` is submitted, all input fields are automatically collected into a dictionary and passed as `event.value`. You can conveniently access fields using square brackets on the event object (e.g., `e["fieldname"]`).
121
+
122
+
123
+ ## History
124
+
125
+ At the beginning, there was [guy](https://github.com/manatlan/guy), which was/is the same concept as [python-eel](https://github.com/ChrisKnott/Eel), but more advanced.
126
+ One day, I've discovered [remi](https://github.com/rawpython/remi), and asked my self, if it could be done in a *guy way*. The POC was very good, so I released
127
+ a version of it, named [gtag](https://github.com/manatlan/gtag). It worked well despite some drawbacks, but was too difficult to maintain. So I decided to rewrite all
128
+ from scratch, while staying away from *guy* (to separate, *rendering* and *runners*)... and [htag](https://github.com/manatlan/htag/tree/v1-legacy) was born. The codebase is very short, concepts are better implemented, and it's very easy to maintain. And now (2026) [htag v2](https://github.com/manatlan/htag) is here (a full rewrite of v1 with antigravity) !
129
+
@@ -24,6 +24,7 @@ htag/runners/chromeapp.py
24
24
  htag/runners/pyscript.py
25
25
  tests/test_cmd.py
26
26
  tests/test_core.py
27
+ tests/test_deprecation_warnings.py
27
28
  tests/test_fallback.py
28
29
  tests/test_lifecycle.py
29
30
  tests/test_memory.py
@@ -9,8 +9,8 @@ include-package-data = true
9
9
 
10
10
  [project]
11
11
  name = "htag"
12
- version = "2.0.0"
13
- description = "A modern, state-of-the-art Python GUI framework"
12
+ version = "2.0.2"
13
+ description = "Python3 GUI toolkit for building 'beautiful' applications for mobile, web, and desktop from a single codebase"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.10"
16
16
  license = {text = "MIT"}
@@ -46,6 +46,7 @@ dev = [
46
46
  "pytest>=9.0.2",
47
47
  "pytest-asyncio>=1.3.0",
48
48
  "pytest-cov>=7.0.0",
49
+ "pytest-playwright>=0.7.2",
49
50
  "ruff>=0.15.4",
50
51
  ]
51
52
 
@@ -0,0 +1,78 @@
1
+ import pytest
2
+ import logging
3
+ from htag import Tag
4
+
5
+ def test_instantiation_no_warning(caplog):
6
+ """
7
+ Ensures that underscore-prefixed attributes during instantiation
8
+ do NOT trigger a deprecation warning.
9
+ """
10
+ with caplog.at_level(logging.WARNING, logger="htag"):
11
+ t = Tag.div(_class="myclass", _onclick="alert(1)")
12
+ assert len(caplog.records) == 0
13
+ assert t["class"] == "myclass"
14
+ assert t["onclick"] == "alert(1)"
15
+
16
+ def test_getattr_warning(caplog):
17
+ """
18
+ Ensures that accessing underscore-prefixed attributes triggers a warning.
19
+ """
20
+ t = Tag.div(_class="myclass")
21
+ caplog.clear()
22
+ with caplog.at_level(logging.WARNING, logger="htag"):
23
+ c = t._class
24
+ assert c == "myclass"
25
+ assert len(caplog.records) == 1
26
+ assert "DEPRECATION" in caplog.text
27
+ assert "Accessing HTML attributes via underscore prefix ('_class')" in caplog.text
28
+
29
+ def test_setattr_warning(caplog):
30
+ """
31
+ Ensures that setting underscore-prefixed attributes triggers a warning.
32
+ """
33
+ t = Tag.div()
34
+ caplog.clear()
35
+ with caplog.at_level(logging.WARNING, logger="htag"):
36
+ t._class = "newclass"
37
+ assert t["class"] == "newclass"
38
+ assert len(caplog.records) == 1
39
+ assert "DEPRECATION" in caplog.text
40
+ assert "Setting HTML attributes via underscore prefix ('_class')" in caplog.text
41
+
42
+ def test_get_event_warning(caplog):
43
+ """
44
+ Ensures that accessing underscore-prefixed events triggers a warning.
45
+ """
46
+ t = Tag.div(_onclick="alert(1)")
47
+ caplog.clear()
48
+ with caplog.at_level(logging.WARNING, logger="htag"):
49
+ oc = t._onclick
50
+ assert oc == "alert(1)"
51
+ assert len(caplog.records) == 1
52
+ assert "DEPRECATION" in caplog.text
53
+ assert "Accessing events via underscore prefix ('_onclick')" in caplog.text
54
+
55
+ def test_set_event_warning(caplog):
56
+ """
57
+ Ensures that setting underscore-prefixed events triggers a warning.
58
+ """
59
+ t = Tag.div()
60
+ caplog.clear()
61
+ with caplog.at_level(logging.WARNING, logger="htag"):
62
+ t._onclick = "alert(2)"
63
+ assert t["onclick"] == "alert(2)"
64
+ assert len(caplog.records) == 1
65
+ assert "DEPRECATION" in caplog.text
66
+ assert "Setting events via underscore prefix ('_onclick')" in caplog.text
67
+
68
+ def test_getattr_no_warning_on_missing(caplog):
69
+ """
70
+ Accessing a non-existent underscore attribute should still raise AttributeError or return what super does,
71
+ but shouldn't warn if it doesn't exist in attrs/events.
72
+ """
73
+ t = Tag.div()
74
+ caplog.clear()
75
+ with caplog.at_level(logging.WARNING, logger="htag"):
76
+ with pytest.raises(AttributeError):
77
+ _ = t._non_existent
78
+ assert len(caplog.records) == 0
@@ -0,0 +1,190 @@
1
+ import pytest
2
+ import json
3
+ from unittest.mock import MagicMock, AsyncMock
4
+ from htag.server import Event, AppRunner as App
5
+ from htag import Tag
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_event_simple_value():
9
+ target = MagicMock()
10
+ msg = {
11
+ "id": "123",
12
+ "event": "custom",
13
+ "data": "hello"
14
+ }
15
+ e = Event(target, msg)
16
+ assert e.value == "hello"
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_event_hashchange_data():
20
+ target = MagicMock()
21
+ msg = {
22
+ "id": "123",
23
+ "event": "hashchange",
24
+ "data": {
25
+ "newURL": "http://localhost/#new",
26
+ "oldURL": "http://localhost/#old",
27
+ "callback_id": "123"
28
+ }
29
+ }
30
+ e = Event(target, msg)
31
+ assert e.newURL == "http://localhost/#new"
32
+ assert e.oldURL == "http://localhost/#old"
33
+
34
+ @pytest.mark.asyncio
35
+ async def test_app_handle_simple_event():
36
+ app = App()
37
+ shared = {"val": None}
38
+
39
+ def my_cb(e):
40
+ shared["val"] = e.value
41
+
42
+ # We simulate a tag that has a custom event handler
43
+ class MyTag(Tag.div):
44
+ def init(self):
45
+ self._oncustom = my_cb
46
+
47
+ tag = MyTag()
48
+ app += tag
49
+
50
+ ws = AsyncMock()
51
+ msg = {
52
+ "id": tag.id,
53
+ "event": "custom",
54
+ "data": "simple_value"
55
+ }
56
+
57
+ await app.handle_event(msg, ws)
58
+ assert shared["val"] == "simple_value"
59
+
60
+ @pytest.mark.asyncio
61
+ async def test_app_handle_hashchange_event():
62
+ app = App()
63
+ shared = {"new": None, "old": None}
64
+
65
+ def on_hash(e):
66
+ shared["new"] = e.newURL
67
+ shared["old"] = e.oldURL
68
+
69
+ app._onhashchange = on_hash
70
+
71
+ ws = AsyncMock()
72
+ msg = {
73
+ "id": app.id,
74
+ "event": "hashchange",
75
+ "data": {
76
+ "newURL": "new_url",
77
+ "oldURL": "old_url"
78
+ }
79
+ }
80
+
81
+ await app.handle_event(msg, ws)
82
+ assert shared["new"] == "new_url"
83
+ assert shared["old"] == "old_url"
84
+
85
+ @pytest.mark.asyncio
86
+ async def test_event_dict_value():
87
+ target = MagicMock()
88
+ msg = {
89
+ "id": "123",
90
+ "event": "submit",
91
+ "data": {
92
+ "name": "manatlan",
93
+ "age": 42
94
+ }
95
+ }
96
+ e = Event(target, msg)
97
+ # Standard htag v2: data is in .value
98
+ assert e.value == {"name": "manatlan", "age": 42}
99
+ # Backward compatibility / Convenience: subscriptable access
100
+ assert e["name"] == "manatlan"
101
+ assert e["age"] == 42
102
+ assert e["missing"] is None
103
+
104
+ @pytest.mark.asyncio
105
+ async def test_event_getitem_fallback_to_attr():
106
+ target = MagicMock()
107
+ msg = {
108
+ "id": "123",
109
+ "event": "click",
110
+ "data": {
111
+ "pageX": 100,
112
+ "pageY": 200
113
+ }
114
+ }
115
+ e = Event(target, msg)
116
+ # Should find in attributes (Event sets attrs from msg['data'])
117
+ assert e["pageX"] == 100
118
+ assert e.pageX == 100
119
+ # Should work via __getitem__ too
120
+ assert e["id"] == "123"
121
+
122
+ @pytest.mark.asyncio
123
+ async def test_app_handle_form_event():
124
+ app = App()
125
+ shared = {"data": None}
126
+
127
+ def on_submit(e):
128
+ shared["data"] = e.value
129
+
130
+ class MyForm(Tag.form):
131
+ def init(self):
132
+ self._onsubmit = on_submit
133
+
134
+ tag = MyForm()
135
+ app += tag
136
+
137
+ ws = AsyncMock()
138
+ msg = {
139
+ "id": tag.id,
140
+ "event": "submit",
141
+ "data": {
142
+ "value": {"field1": "val1", "field2": "val2"}
143
+ }
144
+ }
145
+
146
+ await app.handle_event(msg, ws)
147
+ assert shared["data"] == {"field1": "val1", "field2": "val2"}
148
+
149
+ @pytest.mark.asyncio
150
+ async def test_event_priority_collision():
151
+ target = MagicMock()
152
+ msg = {
153
+ "id": "real_id",
154
+ "event": "submit",
155
+ "data": {
156
+ "id": "form_id", # Collision with event.id
157
+ "name": "bob"
158
+ }
159
+ }
160
+ e = Event(target, msg)
161
+ # The form data 'id' should win in subscript access
162
+ assert e["id"] == "form_id"
163
+ # But event.id (attribute) was overwritten by the flat dict expansion in __init__
164
+ assert e.id == "form_id"
165
+
166
+ @pytest.mark.asyncio
167
+ async def test_event_flat_value_subscript():
168
+ target = MagicMock()
169
+ msg = {
170
+ "id": "123",
171
+ "event": "click",
172
+ "data": "just_a_string"
173
+ }
174
+ e = Event(target, msg)
175
+ assert e.value == "just_a_string"
176
+ # Subscript should fall back to attributes even if value isn't a dict
177
+ assert e["id"] == "123"
178
+ assert e["missing"] is None
179
+
180
+ @pytest.mark.asyncio
181
+ async def test_event_empty_data():
182
+ target = MagicMock()
183
+ msg = {
184
+ "id": "123",
185
+ "event": "click"
186
+ # data is missing
187
+ }
188
+ e = Event(target, msg)
189
+ assert e.value is None
190
+ assert e["id"] == "123"
@@ -1,83 +0,0 @@
1
- import pytest
2
- import json
3
- from unittest.mock import MagicMock, AsyncMock
4
- from htag.server import Event, AppRunner as App
5
- from htag import Tag
6
-
7
- @pytest.mark.asyncio
8
- async def test_event_simple_value():
9
- target = MagicMock()
10
- msg = {
11
- "id": "123",
12
- "event": "custom",
13
- "data": "hello"
14
- }
15
- e = Event(target, msg)
16
- assert e.value == "hello"
17
-
18
- @pytest.mark.asyncio
19
- async def test_event_hashchange_data():
20
- target = MagicMock()
21
- msg = {
22
- "id": "123",
23
- "event": "hashchange",
24
- "data": {
25
- "newURL": "http://localhost/#new",
26
- "oldURL": "http://localhost/#old",
27
- "callback_id": "123"
28
- }
29
- }
30
- e = Event(target, msg)
31
- assert e.newURL == "http://localhost/#new"
32
- assert e.oldURL == "http://localhost/#old"
33
-
34
- @pytest.mark.asyncio
35
- async def test_app_handle_simple_event():
36
- app = App()
37
- shared = {"val": None}
38
-
39
- def my_cb(e):
40
- shared["val"] = e.value
41
-
42
- # We simulate a tag that has a custom event handler
43
- class MyTag(Tag.div):
44
- def init(self):
45
- self._oncustom = my_cb
46
-
47
- tag = MyTag()
48
- app += tag
49
-
50
- ws = AsyncMock()
51
- msg = {
52
- "id": tag.id,
53
- "event": "custom",
54
- "data": "simple_value"
55
- }
56
-
57
- await app.handle_event(msg, ws)
58
- assert shared["val"] == "simple_value"
59
-
60
- @pytest.mark.asyncio
61
- async def test_app_handle_hashchange_event():
62
- app = App()
63
- shared = {"new": None, "old": None}
64
-
65
- def on_hash(e):
66
- shared["new"] = e.newURL
67
- shared["old"] = e.oldURL
68
-
69
- app._onhashchange = on_hash
70
-
71
- ws = AsyncMock()
72
- msg = {
73
- "id": app.id,
74
- "event": "hashchange",
75
- "data": {
76
- "newURL": "new_url",
77
- "oldURL": "old_url"
78
- }
79
- }
80
-
81
- await app.handle_event(msg, ws)
82
- assert shared["new"] == "new_url"
83
- assert shared["old"] == "old_url"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes