hyperclass 0.0.4__tar.gz → 0.0.5__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 (23) hide show
  1. {hyperclass-0.0.4 → hyperclass-0.0.5}/PKG-INFO +9 -3
  2. {hyperclass-0.0.4 → hyperclass-0.0.5}/README.md +8 -2
  3. {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/__init__.py +1 -1
  4. {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/html.py +27 -21
  5. {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/wsgi.py +24 -6
  6. {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass.egg-info/PKG-INFO +9 -3
  7. {hyperclass-0.0.4 → hyperclass-0.0.5}/pyproject.toml +1 -1
  8. {hyperclass-0.0.4 → hyperclass-0.0.5}/tests/test_bookmarks.py +4 -0
  9. {hyperclass-0.0.4 → hyperclass-0.0.5}/tests/test_html.py +12 -5
  10. hyperclass-0.0.5/tests/test_package.py +5 -0
  11. {hyperclass-0.0.4 → hyperclass-0.0.5}/tests/test_wsgi.py +20 -0
  12. hyperclass-0.0.4/tests/test_package.py +0 -5
  13. {hyperclass-0.0.4 → hyperclass-0.0.5}/LICENSE +0 -0
  14. {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/__main__.py +0 -0
  15. {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/css.py +0 -0
  16. {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/htmx.py +0 -0
  17. {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass.egg-info/SOURCES.txt +0 -0
  18. {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass.egg-info/dependency_links.txt +0 -0
  19. {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass.egg-info/requires.txt +0 -0
  20. {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass.egg-info/top_level.txt +0 -0
  21. {hyperclass-0.0.4 → hyperclass-0.0.5}/setup.cfg +0 -0
  22. {hyperclass-0.0.4 → hyperclass-0.0.5}/tests/test_cli.py +0 -0
  23. {hyperclass-0.0.4 → hyperclass-0.0.5}/tests/test_htmx.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperclass
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Build interactive web applications as Python class hierarchies.
5
5
  Author: Grant Jenks
6
6
  License-Expression: Apache-2.0
@@ -239,8 +239,9 @@ class primary_button(button):
239
239
  narrow = media(max_width=40 * rem, width="100%")
240
240
  ~~~
241
241
 
242
- Pages collect only the rules used by their element tree. Python inheritance and
243
- the CSS cascade cooperate instead of imitating one another.
242
+ Pages collect only the rules used by their element tree. Rules use the concrete
243
+ semantic class chain as their selector, so Python inheritance and the CSS
244
+ cascade cooperate even when new component styles arrive later.
244
245
 
245
246
  ## Classes, IDs, and names are selectors
246
247
 
@@ -358,6 +359,11 @@ htmx request sends only the fragment to swap.
358
359
  asset from jsDelivr. htmx 4 `<hx-partial>` responses can update several
359
360
  object-selected regions from one request.
360
361
 
362
+ When an htmx response introduces a component that was not present on the first
363
+ page, Hyperclass includes its CSS in a partial targeting the page's stable
364
+ `hyperclass-styles` stylesheet. The new fragment is styled immediately, without
365
+ a reload or a global CSS build.
366
+
361
367
  ## Try the examples
362
368
 
363
369
  Clone the repository and run the persistent SQLite bookmark inbox:
@@ -209,8 +209,9 @@ class primary_button(button):
209
209
  narrow = media(max_width=40 * rem, width="100%")
210
210
  ~~~
211
211
 
212
- Pages collect only the rules used by their element tree. Python inheritance and
213
- the CSS cascade cooperate instead of imitating one another.
212
+ Pages collect only the rules used by their element tree. Rules use the concrete
213
+ semantic class chain as their selector, so Python inheritance and the CSS
214
+ cascade cooperate even when new component styles arrive later.
214
215
 
215
216
  ## Classes, IDs, and names are selectors
216
217
 
@@ -328,6 +329,11 @@ htmx request sends only the fragment to swap.
328
329
  asset from jsDelivr. htmx 4 `<hx-partial>` responses can update several
329
330
  object-selected regions from one request.
330
331
 
332
+ When an htmx response introduces a component that was not present on the first
333
+ page, Hyperclass includes its CSS in a partial targeting the page's stable
334
+ `hyperclass-styles` stylesheet. The new fragment is styled immediately, without
335
+ a reload or a global CSS build.
336
+
331
337
  ## Try the examples
332
338
 
333
339
  Clone the repository and run the persistent SQLite bookmark inbox:
@@ -62,7 +62,7 @@ from .wsgi import (
62
62
  )
63
63
  from .wsgi import delete as delete_route
64
64
 
65
- __version__ = "0.0.4"
65
+ __version__ = "0.0.5"
66
66
 
67
67
  hidden = "hidden"
68
68
  submit = "submit"
@@ -173,33 +173,38 @@ def fragment(*children: Any) -> Fragment:
173
173
 
174
174
  class RenderContext:
175
175
  def __init__(self) -> None:
176
- self.styled_classes: list[type] = []
177
- self._seen_styles: set[type] = set()
176
+ self.styled_elements: list[type] = []
177
+ self._seen_elements: set[type] = set()
178
178
 
179
179
  def register(self, value: type) -> None:
180
- if value in self._seen_styles:
180
+ if value in self._seen_elements:
181
181
  return
182
+ classes = semantic_classes(value)
182
183
  if any(
183
184
  isinstance(rule, (Style, Media))
184
- for rule in value.__dict__.values()
185
+ for semantic_class in classes
186
+ for rule in semantic_class.__dict__.values()
185
187
  ):
186
- self._seen_styles.add(value)
187
- self.styled_classes.append(value)
188
+ self._seen_elements.add(value)
189
+ self.styled_elements.append(value)
188
190
 
189
191
  def stylesheet(self) -> str:
190
192
  rules: list[str] = []
191
- for value in self.styled_classes:
192
- base = f".{class_name(value)}"
193
- for name, rule in value.__dict__.items():
194
- if name == "style" and isinstance(rule, Style):
195
- rules.append(f"{base}{{{rule.render()}}}")
196
- elif name in PSEUDO_STATES and isinstance(rule, Style):
197
- state = name.replace("_", "-")
198
- rules.append(f"{base}:{state}{{{rule.render()}}}")
199
- elif isinstance(rule, Media):
200
- rules.append(
201
- f"@media {rule.query()}{{{base}{{{rule.style.render()}}}}}"
202
- )
193
+ for element_type in self.styled_elements:
194
+ classes = semantic_classes(element_type)
195
+ base = "".join(f".{class_name(value)}" for value in classes)
196
+ for value in classes:
197
+ for name, rule in value.__dict__.items():
198
+ if name == "style" and isinstance(rule, Style):
199
+ rules.append(f"{base}{{{rule.render()}}}")
200
+ elif name in PSEUDO_STATES and isinstance(rule, Style):
201
+ state = name.replace("_", "-")
202
+ rules.append(f"{base}:{state}{{{rule.render()}}}")
203
+ elif isinstance(rule, Media):
204
+ rules.append(
205
+ f"@media {rule.query()}"
206
+ f"{{{base}{{{rule.style.render()}}}}}"
207
+ )
203
208
  return "".join(rules)
204
209
 
205
210
 
@@ -340,8 +345,7 @@ def _render(value: Any, context: RenderContext) -> str:
340
345
  value_type = type(value)
341
346
  tag = _tag_for(value_type)
342
347
  classes = semantic_classes(value_type)
343
- for semantic_class in classes:
344
- context.register(semantic_class)
348
+ context.register(value_type)
345
349
  attributes = _render_attributes(value, classes)
346
350
  if tag in VOID_TAGS:
347
351
  return f"<{tag}{attributes}>"
@@ -383,7 +387,9 @@ class Page:
383
387
  body_html = _render(self.children, context)
384
388
  head_html = _render(self.head, context)
385
389
  stylesheet = context.stylesheet()
386
- style_html = f"<style>{stylesheet}</style>" if stylesheet else ""
390
+ style_html = (
391
+ f'<style id="{id.hyperclass_styles}">{stylesheet}</style>'
392
+ )
387
393
  script_html = ""
388
394
  if self.htmx:
389
395
  script_html = (
@@ -13,7 +13,16 @@ from typing import Any, Union, get_args, get_origin, get_type_hints
13
13
  from urllib.parse import parse_qs, quote, urlencode
14
14
  from wsgiref.simple_server import make_server
15
15
 
16
- from .html import Fragment, Page, element, render
16
+ from .html import (
17
+ Fragment,
18
+ Page,
19
+ RenderContext,
20
+ element,
21
+ id,
22
+ markup,
23
+ partial,
24
+ render,
25
+ )
17
26
 
18
27
  Handler = Callable[..., Any]
19
28
  StartResponse = Callable[[str, list[tuple[str, str]]], Any]
@@ -479,11 +488,20 @@ class App:
479
488
  if isinstance(body, Page):
480
489
  text = body.render()
481
490
  elif isinstance(body, (element, Fragment)):
482
- text = (
483
- render(body)
484
- if request.is_htmx
485
- else Page(body, title=self.title).render()
486
- )
491
+ if request.is_htmx:
492
+ context = RenderContext()
493
+ text = render(body, context=context)
494
+ stylesheet = context.stylesheet()
495
+ if stylesheet:
496
+ text += render(
497
+ partial(
498
+ markup(stylesheet),
499
+ id=id.hyperclass_styles,
500
+ hx_swap="append",
501
+ )
502
+ )
503
+ else:
504
+ text = Page(body, title=self.title).render()
487
505
  else:
488
506
  text = str(body)
489
507
  payload = text.encode("utf-8")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperclass
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Build interactive web applications as Python class hierarchies.
5
5
  Author: Grant Jenks
6
6
  License-Expression: Apache-2.0
@@ -239,8 +239,9 @@ class primary_button(button):
239
239
  narrow = media(max_width=40 * rem, width="100%")
240
240
  ~~~
241
241
 
242
- Pages collect only the rules used by their element tree. Python inheritance and
243
- the CSS cascade cooperate instead of imitating one another.
242
+ Pages collect only the rules used by their element tree. Rules use the concrete
243
+ semantic class chain as their selector, so Python inheritance and the CSS
244
+ cascade cooperate even when new component styles arrive later.
244
245
 
245
246
  ## Classes, IDs, and names are selectors
246
247
 
@@ -358,6 +359,11 @@ htmx request sends only the fragment to swap.
358
359
  asset from jsDelivr. htmx 4 `<hx-partial>` responses can update several
359
360
  object-selected regions from one request.
360
361
 
362
+ When an htmx response introduces a component that was not present on the first
363
+ page, Hyperclass includes its CSS in a partial targeting the page's stable
364
+ `hyperclass-styles` stylesheet. The new fragment is styled immediately, without
365
+ a reload or a global CSS build.
366
+
361
367
  ## Try the examples
362
368
 
363
369
  Clone the repository and run the persistent SQLite bookmark inbox:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperclass"
3
- version = "0.0.4"
3
+ version = "0.0.5"
4
4
  description = "Build interactive web applications as Python class hierarchies."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -38,6 +38,8 @@ def test_bookmark_lifecycle(tmp_path):
38
38
  assert payload.startswith("<!doctype html>")
39
39
  assert "Bookmark inbox" in payload
40
40
  assert "0 unread bookmarks" in payload
41
+ assert '<style id="hyperclass-styles">' in payload
42
+ assert ".bookmark-card" not in payload
41
43
  assert 'content="width=device-width, initial-scale=1"' in payload
42
44
  assert (
43
45
  "@media (max-width:40rem){.bookmark-form{grid-template-columns:1fr}}"
@@ -61,6 +63,8 @@ def test_bookmark_lifecycle(tmp_path):
61
63
  )
62
64
  assert 'target="_blank" rel="noreferrer"' in payload
63
65
  assert 'hx-patch="/bookmarks/1"' in payload
66
+ assert '<hx-partial id="hyperclass-styles" hx-swap="append">' in payload
67
+ assert ".bookmark-card.unread.unread-bookmark{display:grid" in payload
64
68
  bookmark = app.store.list()[0]
65
69
 
66
70
  captured, payload = request(
@@ -67,9 +67,14 @@ def test_page_collects_styles_and_pins_htmx_4():
67
67
  document = Page(warning_card("Careful"), title="Demo").render()
68
68
  assert document.startswith("<!doctype html>")
69
69
  assert "<title>Demo</title>" in document
70
- assert ".card{display:grid;gap:1rem;padding:1.25rem}" in document
70
+ assert '<style id="hyperclass-styles">' in document
71
71
  assert (
72
- ".warning-card{border-color:orange;background:rgb(255 165 0 / 0.08)}"
72
+ ".card.warning-card{display:grid;gap:1rem;padding:1.25rem}"
73
+ in document
74
+ )
75
+ assert (
76
+ ".card.warning-card{border-color:orange;"
77
+ "background:rgb(255 165 0 / 0.08)}"
73
78
  in document
74
79
  )
75
80
  assert "https://cdn.jsdelivr.net/npm/htmx.org@4.0.0" in document
@@ -103,12 +108,14 @@ def test_page_collects_state_and_media_styles_from_classes():
103
108
  narrow = media(max_width=40 * rem, grid_template_columns="1fr")
104
109
 
105
110
  document = Page(interactive_card()).render()
106
- assert ".interactive-card:hover{background:lavender}" in document
111
+ assert ".card.interactive-card:hover{background:lavender}" in document
107
112
  assert (
108
- ".interactive-card:focus-visible{outline:2px solid purple}" in document
113
+ ".card.interactive-card:focus-visible{outline:2px solid purple}"
114
+ in document
109
115
  )
110
116
  assert (
111
- "@media (max-width:40rem){.interactive-card{grid-template-columns:1fr}}"
117
+ "@media (max-width:40rem){.card.interactive-card"
118
+ "{grid-template-columns:1fr}}"
112
119
  in document
113
120
  )
114
121
 
@@ -0,0 +1,5 @@
1
+ import hyperclass
2
+
3
+
4
+ def test_version():
5
+ assert hyperclass.__version__ == "0.0.5"
@@ -8,6 +8,7 @@ from hyperclass import (
8
8
  Values,
9
9
  button,
10
10
  closest,
11
+ css,
11
12
  div,
12
13
  form,
13
14
  get,
@@ -276,3 +277,22 @@ def test_values_accept_first_class_form_names():
276
277
  assert values.get(name.search_query) == "wsgi"
277
278
  assert values.getlist(name.search_query) == ["python", "wsgi"]
278
279
  assert values.int(name.page) == 3
280
+
281
+
282
+ def test_htmx_fragment_appends_required_styles_to_page_stylesheet():
283
+ class styled_result(div):
284
+ style = css(color="purple")
285
+
286
+ app = App()
287
+
288
+ @app.get("/result")
289
+ def result(request):
290
+ return styled_result("Found")
291
+
292
+ captured, payload = request(app, path="/result", htmx=True)
293
+ assert captured["status"] == "200 OK"
294
+ assert payload == (
295
+ '<div class="styled-result">Found</div>'
296
+ '<hx-partial id="hyperclass-styles" hx-swap="append">'
297
+ ".styled-result{color:purple}</hx-partial>"
298
+ )
@@ -1,5 +0,0 @@
1
- import hyperclass
2
-
3
-
4
- def test_version():
5
- assert hyperclass.__version__ == "0.0.4"
File without changes
File without changes
File without changes
File without changes