htag 2.0.5__tar.gz → 2.0.7__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 (47) hide show
  1. {htag-2.0.5 → htag-2.0.7}/PKG-INFO +1 -1
  2. {htag-2.0.5 → htag-2.0.7}/htag/client_js.py +35 -3
  3. {htag-2.0.5 → htag-2.0.7}/htag/core.py +4 -1
  4. {htag-2.0.5 → htag-2.0.7}/htag/runner.py +16 -2
  5. {htag-2.0.5 → htag-2.0.7}/htag.egg-info/PKG-INFO +1 -1
  6. {htag-2.0.5 → htag-2.0.7}/htag.egg-info/SOURCES.txt +1 -0
  7. {htag-2.0.5 → htag-2.0.7}/pyproject.toml +1 -1
  8. htag-2.0.7/tests/test_statics_deduplication.py +106 -0
  9. {htag-2.0.5 → htag-2.0.7}/README.md +0 -0
  10. {htag-2.0.5 → htag-2.0.7}/htag/__init__.py +0 -0
  11. {htag-2.0.5 → htag-2.0.7}/htag/cli.py +0 -0
  12. {htag-2.0.5 → htag-2.0.7}/htag/context.py +0 -0
  13. {htag-2.0.5 → htag-2.0.7}/htag/css.py +0 -0
  14. {htag-2.0.5 → htag-2.0.7}/htag/exceptions.py +0 -0
  15. {htag-2.0.5 → htag-2.0.7}/htag/logo.py +0 -0
  16. {htag-2.0.5 → htag-2.0.7}/htag/runners/__init__.py +0 -0
  17. {htag-2.0.5 → htag-2.0.7}/htag/runners/chromeapp.py +0 -0
  18. {htag-2.0.5 → htag-2.0.7}/htag/runners/pyscript.py +0 -0
  19. {htag-2.0.5 → htag-2.0.7}/htag/server.py +0 -0
  20. {htag-2.0.5 → htag-2.0.7}/htag/tag.py +0 -0
  21. {htag-2.0.5 → htag-2.0.7}/htag/utils.py +0 -0
  22. {htag-2.0.5 → htag-2.0.7}/htag/web.py +0 -0
  23. {htag-2.0.5 → htag-2.0.7}/htag.egg-info/dependency_links.txt +0 -0
  24. {htag-2.0.5 → htag-2.0.7}/htag.egg-info/entry_points.txt +0 -0
  25. {htag-2.0.5 → htag-2.0.7}/htag.egg-info/requires.txt +0 -0
  26. {htag-2.0.5 → htag-2.0.7}/htag.egg-info/top_level.txt +0 -0
  27. {htag-2.0.5 → htag-2.0.7}/setup.cfg +0 -0
  28. {htag-2.0.5 → htag-2.0.7}/tests/test_cmd.py +0 -0
  29. {htag-2.0.5 → htag-2.0.7}/tests/test_core.py +0 -0
  30. {htag-2.0.5 → htag-2.0.7}/tests/test_deprecation_warnings.py +0 -0
  31. {htag-2.0.5 → htag-2.0.7}/tests/test_fallback.py +0 -0
  32. {htag-2.0.5 → htag-2.0.7}/tests/test_interacting_robustness.py +0 -0
  33. {htag-2.0.5 → htag-2.0.7}/tests/test_lifecycle.py +0 -0
  34. {htag-2.0.5 → htag-2.0.7}/tests/test_memory.py +0 -0
  35. {htag-2.0.5 → htag-2.0.7}/tests/test_parano.py +0 -0
  36. {htag-2.0.5 → htag-2.0.7}/tests/test_reactivity_edge_cases.py +0 -0
  37. {htag-2.0.5 → htag-2.0.7}/tests/test_runner_pyscript.py +0 -0
  38. {htag-2.0.5 → htag-2.0.7}/tests/test_runners_reload.py +0 -0
  39. {htag-2.0.5 → htag-2.0.7}/tests/test_server.py +0 -0
  40. {htag-2.0.5 → htag-2.0.7}/tests/test_simple_events.py +0 -0
  41. {htag-2.0.5 → htag-2.0.7}/tests/test_state_advanced.py +0 -0
  42. {htag-2.0.5 → htag-2.0.7}/tests/test_state_features.py +0 -0
  43. {htag-2.0.5 → htag-2.0.7}/tests/test_state_proxy.py +0 -0
  44. {htag-2.0.5 → htag-2.0.7}/tests/test_state_reactivity.py +0 -0
  45. {htag-2.0.5 → htag-2.0.7}/tests/test_tag_names.py +0 -0
  46. {htag-2.0.5 → htag-2.0.7}/tests/test_web_sessions.py +0 -0
  47. {htag-2.0.5 → htag-2.0.7}/tests/test_webapp_run.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htag
3
- Version: 2.0.5
3
+ Version: 2.0.7
4
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
@@ -1,4 +1,12 @@
1
- CLIENT_JS = """
1
+ def __minify_js(js_code: str) -> str:
2
+ import re
3
+ # Remove single line comments (but not URL schemes like http://)
4
+ js = re.sub(r'(?<!:)//.*', '', js_code)
5
+ # Remove newlines and tabs
6
+ js = re.sub(r'\s+', ' ', js).strip()
7
+ return js
8
+
9
+ CLIENT_JS = __minify_js("""
2
10
  // The client-side bridge that connects the browser to the Python server.
3
11
  var ws;
4
12
  var use_fallback = false;
@@ -158,7 +166,30 @@ function handle_payload(data) {
158
166
  // Apply partial DOM updates received from the server
159
167
  for(var id in data.updates) {
160
168
  var el = document.getElementById(id) || document.querySelector('[data-htag-id="' + id + '"]');
161
- if(el) el.outerHTML = data.updates[id];
169
+ if(el) {
170
+ if(el.tagName === 'BODY') {
171
+ var doc = new DOMParser().parseFromString(data.updates[id], 'text/html');
172
+
173
+ // Sync attributes properly
174
+ var newAttrNames = new Set();
175
+ for(var i = 0; i < doc.body.attributes.length; i++) {
176
+ var attr = doc.body.attributes[i];
177
+ el.setAttribute(attr.name, attr.value);
178
+ newAttrNames.add(attr.name);
179
+ }
180
+ // Remove old attributes that are not in the new body
181
+ for(var i = el.attributes.length - 1; i >= 0; i--) {
182
+ var attrName = el.attributes[i].name;
183
+ if(!newAttrNames.has(attrName)) {
184
+ el.removeAttribute(attrName);
185
+ }
186
+ }
187
+
188
+ el.innerHTML = doc.body.innerHTML;
189
+ } else {
190
+ el.outerHTML = data.updates[id];
191
+ }
192
+ }
162
193
  }
163
194
 
164
195
  // Ensure overlays are still in the DOM (in case the body was replaced)
@@ -349,4 +380,5 @@ function htag_event(id, event_name, event) {
349
380
  window.htag_transport(payload);
350
381
  });
351
382
  }
352
- """
383
+ """)
384
+
@@ -438,7 +438,10 @@ class GTag: # aka "Generic Tag"
438
438
  else:
439
439
  self.tag = "div" # fallback
440
440
 
441
- self.id = f"{self.tag}-{id(self)}"
441
+ if getattr(self, "tag", None) in ("style", "script"):
442
+ self.id = ""
443
+ else:
444
+ self.id = f"{self.tag}-{id(self)}"
442
445
  logger.debug("Created Tag: %s (id: %s)", self.tag, self.id)
443
446
 
444
447
  # Scoped style: auto-prefix CSS rules with a unique class per component class
@@ -339,16 +339,30 @@ class AppRunner(BaseApp):
339
339
 
340
340
  def collect_statics(self, tag: GTag, result: list[str]) -> None:
341
341
  """Recursively collects statics from the whole tag tree."""
342
+ seen_ids = set()
343
+ seen_contents = set(result)
342
344
 
343
345
  def visitor(t: GTag) -> None:
344
346
  s_instance = getattr(t, "statics", [])
345
347
  s_class = getattr(t.__class__, "statics", [])
346
- for s_list in [s_class, s_instance]:
348
+
349
+ # Avoid processing the exact same list twice (common case)
350
+ lists = [s_class]
351
+ if s_instance is not s_class:
352
+ lists.append(s_instance)
353
+
354
+ for s_list in lists:
347
355
  if not isinstance(s_list, (list, tuple)):
348
356
  s_list = [s_list]
349
357
  for s in s_list:
358
+ sid = id(s)
359
+ if sid in seen_ids:
360
+ continue
361
+ seen_ids.add(sid)
362
+
350
363
  s_str = str(s)
351
- if s_str not in result:
364
+ if s_str not in seen_contents:
365
+ seen_contents.add(s_str)
352
366
  result.append(s_str)
353
367
 
354
368
  self._walk_tree(tag, visitor)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htag
3
- Version: 2.0.5
3
+ Version: 2.0.7
4
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
@@ -39,6 +39,7 @@ tests/test_state_advanced.py
39
39
  tests/test_state_features.py
40
40
  tests/test_state_proxy.py
41
41
  tests/test_state_reactivity.py
42
+ tests/test_statics_deduplication.py
42
43
  tests/test_tag_names.py
43
44
  tests/test_web_sessions.py
44
45
  tests/test_webapp_run.py
@@ -9,7 +9,7 @@ include-package-data = true
9
9
 
10
10
  [project]
11
11
  name = "htag"
12
- version = "2.0.5"
12
+ version = "2.0.7"
13
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"
@@ -0,0 +1,106 @@
1
+ from htag import Tag, App
2
+
3
+ def test_statics_deduplication_same_object():
4
+ # A shared static object
5
+ SHARED_JS = Tag.script("console.log('shared')")
6
+
7
+ class Comp1(Tag.div):
8
+ statics = [SHARED_JS]
9
+ class Comp2(Tag.div):
10
+ statics = [SHARED_JS]
11
+
12
+ class MyApp(App):
13
+ def init(self):
14
+ self += Comp1()
15
+ self += Comp2()
16
+
17
+ app = MyApp()
18
+ html = app._render_page()
19
+
20
+ # Count occurrences of SHARED_JS in html
21
+ count = html.count("console.log('shared')")
22
+ assert count == 1, "Deduplication failed for same object"
23
+
24
+ def test_statics_deduplication_identical_content():
25
+ # Identical but different objects
26
+ JS1 = Tag.script("console.log('identical')")
27
+ JS2 = Tag.script("console.log('identical')")
28
+
29
+ class Comp3(Tag.div):
30
+ statics = [JS1]
31
+ class Comp4(Tag.div):
32
+ statics = [JS2]
33
+
34
+ app = App(Comp3(), Comp4())
35
+ html = app._render_page()
36
+ count = html.count("console.log('identical')")
37
+ assert count == 1, "Deduplication failed for identical content"
38
+
39
+ def test_statics_inheritance_optimization():
40
+ SHARED_STYLE = Tag.style("body { color: red; }")
41
+
42
+ class Comp(Tag.div):
43
+ statics = [SHARED_STYLE]
44
+
45
+ c = Comp()
46
+ # By default, c.statics is the same object as Comp.statics
47
+ assert c.statics is Comp.statics
48
+
49
+ app = App(c)
50
+ html = app._render_page()
51
+ count = html.count("body { color: red; }")
52
+ assert count == 1, "Deduplication failed for class/instance shared statics"
53
+
54
+ def test_script_style_no_auto_id():
55
+ # Verify that script and style tags don't have auto-generated IDs
56
+ # which used to break content deduplication.
57
+ s = Tag.script("alert(1)")
58
+ st = Tag.style(".foo {}")
59
+ d = Tag.div("hello")
60
+
61
+ assert s.id == ""
62
+ assert st.id == ""
63
+ assert d.id.startswith("div-")
64
+
65
+ assert 'id=""' in str(s) or 'id' not in str(s)
66
+ assert 'id=""' in str(st) or 'id' not in str(st)
67
+ assert f'id="{d.id}"' in str(d)
68
+
69
+ def test_statics_collection_with_mixed_types():
70
+ # Test that collect_statics handles non-list statics gracefully
71
+ class Comp(Tag.div):
72
+ statics = Tag.script("console.log('single')")
73
+
74
+ app = App(Comp())
75
+ html = app._render_page()
76
+ assert "console.log('single')" in html
77
+
78
+ def test_statics_deduplication_across_renders():
79
+ # Verify that App.sent_statics works and prevents re-sending same statics
80
+ SHARED = Tag.script("console.log('shared')")
81
+
82
+ class MyApp(App):
83
+ statics = [SHARED]
84
+ def init(self):
85
+ self.main = Tag.div()
86
+ self += self.main
87
+
88
+ app = MyApp()
89
+ # Initial render: collects SHARED and puts it in sent_statics
90
+ app._render_page()
91
+ assert SHARED in app.sent_statics or str(SHARED) in app.sent_statics
92
+
93
+ # Simulate a dynamic update adding a component with the same static (different instance/list)
94
+ class Comp(Tag.div):
95
+ statics = [SHARED]
96
+
97
+ app.main.add(Comp())
98
+
99
+ # broadcast_updates logic (manual verification)
100
+ all_statics = []
101
+ app.collect_statics(app, all_statics) # Should collect SHARED again
102
+
103
+ # But new_statics calculation should skip it if already in sent_statics
104
+ new_statics = [s for s in all_statics if s not in app.sent_statics]
105
+
106
+ assert "console.log('shared')" not in "".join(new_statics)
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
File without changes
File without changes