htag 2.0.5__tar.gz → 2.0.6__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.6}/PKG-INFO +1 -1
  2. {htag-2.0.5 → htag-2.0.6}/htag/core.py +4 -1
  3. {htag-2.0.5 → htag-2.0.6}/htag/runner.py +16 -2
  4. {htag-2.0.5 → htag-2.0.6}/htag.egg-info/PKG-INFO +1 -1
  5. {htag-2.0.5 → htag-2.0.6}/htag.egg-info/SOURCES.txt +1 -0
  6. {htag-2.0.5 → htag-2.0.6}/pyproject.toml +1 -1
  7. htag-2.0.6/tests/test_statics_deduplication.py +106 -0
  8. {htag-2.0.5 → htag-2.0.6}/README.md +0 -0
  9. {htag-2.0.5 → htag-2.0.6}/htag/__init__.py +0 -0
  10. {htag-2.0.5 → htag-2.0.6}/htag/cli.py +0 -0
  11. {htag-2.0.5 → htag-2.0.6}/htag/client_js.py +0 -0
  12. {htag-2.0.5 → htag-2.0.6}/htag/context.py +0 -0
  13. {htag-2.0.5 → htag-2.0.6}/htag/css.py +0 -0
  14. {htag-2.0.5 → htag-2.0.6}/htag/exceptions.py +0 -0
  15. {htag-2.0.5 → htag-2.0.6}/htag/logo.py +0 -0
  16. {htag-2.0.5 → htag-2.0.6}/htag/runners/__init__.py +0 -0
  17. {htag-2.0.5 → htag-2.0.6}/htag/runners/chromeapp.py +0 -0
  18. {htag-2.0.5 → htag-2.0.6}/htag/runners/pyscript.py +0 -0
  19. {htag-2.0.5 → htag-2.0.6}/htag/server.py +0 -0
  20. {htag-2.0.5 → htag-2.0.6}/htag/tag.py +0 -0
  21. {htag-2.0.5 → htag-2.0.6}/htag/utils.py +0 -0
  22. {htag-2.0.5 → htag-2.0.6}/htag/web.py +0 -0
  23. {htag-2.0.5 → htag-2.0.6}/htag.egg-info/dependency_links.txt +0 -0
  24. {htag-2.0.5 → htag-2.0.6}/htag.egg-info/entry_points.txt +0 -0
  25. {htag-2.0.5 → htag-2.0.6}/htag.egg-info/requires.txt +0 -0
  26. {htag-2.0.5 → htag-2.0.6}/htag.egg-info/top_level.txt +0 -0
  27. {htag-2.0.5 → htag-2.0.6}/setup.cfg +0 -0
  28. {htag-2.0.5 → htag-2.0.6}/tests/test_cmd.py +0 -0
  29. {htag-2.0.5 → htag-2.0.6}/tests/test_core.py +0 -0
  30. {htag-2.0.5 → htag-2.0.6}/tests/test_deprecation_warnings.py +0 -0
  31. {htag-2.0.5 → htag-2.0.6}/tests/test_fallback.py +0 -0
  32. {htag-2.0.5 → htag-2.0.6}/tests/test_interacting_robustness.py +0 -0
  33. {htag-2.0.5 → htag-2.0.6}/tests/test_lifecycle.py +0 -0
  34. {htag-2.0.5 → htag-2.0.6}/tests/test_memory.py +0 -0
  35. {htag-2.0.5 → htag-2.0.6}/tests/test_parano.py +0 -0
  36. {htag-2.0.5 → htag-2.0.6}/tests/test_reactivity_edge_cases.py +0 -0
  37. {htag-2.0.5 → htag-2.0.6}/tests/test_runner_pyscript.py +0 -0
  38. {htag-2.0.5 → htag-2.0.6}/tests/test_runners_reload.py +0 -0
  39. {htag-2.0.5 → htag-2.0.6}/tests/test_server.py +0 -0
  40. {htag-2.0.5 → htag-2.0.6}/tests/test_simple_events.py +0 -0
  41. {htag-2.0.5 → htag-2.0.6}/tests/test_state_advanced.py +0 -0
  42. {htag-2.0.5 → htag-2.0.6}/tests/test_state_features.py +0 -0
  43. {htag-2.0.5 → htag-2.0.6}/tests/test_state_proxy.py +0 -0
  44. {htag-2.0.5 → htag-2.0.6}/tests/test_state_reactivity.py +0 -0
  45. {htag-2.0.5 → htag-2.0.6}/tests/test_tag_names.py +0 -0
  46. {htag-2.0.5 → htag-2.0.6}/tests/test_web_sessions.py +0 -0
  47. {htag-2.0.5 → htag-2.0.6}/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.6
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
@@ -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.6
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.6"
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
File without changes