htag 2.0.0__tar.gz → 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {htag-2.0.0 → htag-2.0.1}/PKG-INFO +35 -12
  2. {htag-2.0.0 → htag-2.0.1}/README.md +33 -10
  3. {htag-2.0.0 → htag-2.0.1}/htag/cli.py +1 -1
  4. {htag-2.0.0 → htag-2.0.1}/htag/client_js.py +48 -15
  5. {htag-2.0.0 → htag-2.0.1}/htag/runner.py +11 -6
  6. {htag-2.0.0 → htag-2.0.1}/htag/web.py +2 -6
  7. {htag-2.0.0 → htag-2.0.1}/htag.egg-info/PKG-INFO +35 -12
  8. {htag-2.0.0 → htag-2.0.1}/pyproject.toml +3 -2
  9. htag-2.0.1/tests/test_simple_events.py +190 -0
  10. htag-2.0.0/tests/test_simple_events.py +0 -83
  11. {htag-2.0.0 → htag-2.0.1}/htag/__init__.py +0 -0
  12. {htag-2.0.0 → htag-2.0.1}/htag/context.py +0 -0
  13. {htag-2.0.0 → htag-2.0.1}/htag/core.py +0 -0
  14. {htag-2.0.0 → htag-2.0.1}/htag/css.py +0 -0
  15. {htag-2.0.0 → htag-2.0.1}/htag/exceptions.py +0 -0
  16. {htag-2.0.0 → htag-2.0.1}/htag/logo.py +0 -0
  17. {htag-2.0.0 → htag-2.0.1}/htag/runners/__init__.py +0 -0
  18. {htag-2.0.0 → htag-2.0.1}/htag/runners/chromeapp.py +0 -0
  19. {htag-2.0.0 → htag-2.0.1}/htag/runners/pyscript.py +0 -0
  20. {htag-2.0.0 → htag-2.0.1}/htag/server.py +0 -0
  21. {htag-2.0.0 → htag-2.0.1}/htag/tag.py +0 -0
  22. {htag-2.0.0 → htag-2.0.1}/htag/utils.py +0 -0
  23. {htag-2.0.0 → htag-2.0.1}/htag.egg-info/SOURCES.txt +0 -0
  24. {htag-2.0.0 → htag-2.0.1}/htag.egg-info/dependency_links.txt +0 -0
  25. {htag-2.0.0 → htag-2.0.1}/htag.egg-info/entry_points.txt +0 -0
  26. {htag-2.0.0 → htag-2.0.1}/htag.egg-info/requires.txt +0 -0
  27. {htag-2.0.0 → htag-2.0.1}/htag.egg-info/top_level.txt +0 -0
  28. {htag-2.0.0 → htag-2.0.1}/setup.cfg +0 -0
  29. {htag-2.0.0 → htag-2.0.1}/tests/test_cmd.py +0 -0
  30. {htag-2.0.0 → htag-2.0.1}/tests/test_core.py +0 -0
  31. {htag-2.0.0 → htag-2.0.1}/tests/test_fallback.py +0 -0
  32. {htag-2.0.0 → htag-2.0.1}/tests/test_lifecycle.py +0 -0
  33. {htag-2.0.0 → htag-2.0.1}/tests/test_memory.py +0 -0
  34. {htag-2.0.0 → htag-2.0.1}/tests/test_parano.py +0 -0
  35. {htag-2.0.0 → htag-2.0.1}/tests/test_reactivity_edge_cases.py +0 -0
  36. {htag-2.0.0 → htag-2.0.1}/tests/test_runner_pyscript.py +0 -0
  37. {htag-2.0.0 → htag-2.0.1}/tests/test_runners_reload.py +0 -0
  38. {htag-2.0.0 → htag-2.0.1}/tests/test_server.py +0 -0
  39. {htag-2.0.0 → htag-2.0.1}/tests/test_state_advanced.py +0 -0
  40. {htag-2.0.0 → htag-2.0.1}/tests/test_state_features.py +0 -0
  41. {htag-2.0.0 → htag-2.0.1}/tests/test_state_proxy.py +0 -0
  42. {htag-2.0.0 → htag-2.0.1}/tests/test_state_reactivity.py +0 -0
  43. {htag-2.0.0 → htag-2.0.1}/tests/test_tag_names.py +0 -0
  44. {htag-2.0.0 → htag-2.0.1}/tests/test_web_sessions.py +0 -0
  45. {htag-2.0.0 → htag-2.0.1}/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.1
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,21 +88,19 @@ 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
106
  * **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`).
@@ -105,3 +118,13 @@ htag is a Python library for building web applications using HTML, CSS, and Java
105
118
  * **Advanced Tag Search (`.find_tag()`)**: Effortlessly locate any component in the tree by its internal htag ID or its manually assigned HTML `id`.
106
119
  * **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
120
  * **Automatic Attribute Assignment**: Non-prefixed keyword arguments passed during component instantiation are automatically assigned as instance attributes, simplifying data passing to custom components.
121
+ * **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"]`).
122
+
123
+
124
+ ## History
125
+
126
+ 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.
127
+ 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
128
+ 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
129
+ 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) !
130
+
@@ -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,21 +67,19 @@ 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
85
  * **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`).
@@ -84,3 +97,13 @@ htag is a Python library for building web applications using HTML, CSS, and Java
84
97
  * **Advanced Tag Search (`.find_tag()`)**: Effortlessly locate any component in the tree by its internal htag ID or its manually assigned HTML `id`.
85
98
  * **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
99
  * **Automatic Attribute Assignment**: Non-prefixed keyword arguments passed during component instantiation are automatically assigned as instance attributes, simplifying data passing to custom components.
100
+ * **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"]`).
101
+
102
+
103
+ ## History
104
+
105
+ 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.
106
+ 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
107
+ 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
108
+ 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) !
109
+
@@ -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) {
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 => {
@@ -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
@@ -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.1
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,21 +88,19 @@ 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
106
  * **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`).
@@ -105,3 +118,13 @@ htag is a Python library for building web applications using HTML, CSS, and Java
105
118
  * **Advanced Tag Search (`.find_tag()`)**: Effortlessly locate any component in the tree by its internal htag ID or its manually assigned HTML `id`.
106
119
  * **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
120
  * **Automatic Attribute Assignment**: Non-prefixed keyword arguments passed during component instantiation are automatically assigned as instance attributes, simplifying data passing to custom components.
121
+ * **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"]`).
122
+
123
+
124
+ ## History
125
+
126
+ 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.
127
+ 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
128
+ 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
129
+ 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) !
130
+
@@ -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.1"
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,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
File without changes
File without changes
File without changes