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.
- {hyperclass-0.0.4 → hyperclass-0.0.5}/PKG-INFO +9 -3
- {hyperclass-0.0.4 → hyperclass-0.0.5}/README.md +8 -2
- {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/__init__.py +1 -1
- {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/html.py +27 -21
- {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/wsgi.py +24 -6
- {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass.egg-info/PKG-INFO +9 -3
- {hyperclass-0.0.4 → hyperclass-0.0.5}/pyproject.toml +1 -1
- {hyperclass-0.0.4 → hyperclass-0.0.5}/tests/test_bookmarks.py +4 -0
- {hyperclass-0.0.4 → hyperclass-0.0.5}/tests/test_html.py +12 -5
- hyperclass-0.0.5/tests/test_package.py +5 -0
- {hyperclass-0.0.4 → hyperclass-0.0.5}/tests/test_wsgi.py +20 -0
- hyperclass-0.0.4/tests/test_package.py +0 -5
- {hyperclass-0.0.4 → hyperclass-0.0.5}/LICENSE +0 -0
- {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/__main__.py +0 -0
- {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/css.py +0 -0
- {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass/htmx.py +0 -0
- {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass.egg-info/SOURCES.txt +0 -0
- {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass.egg-info/dependency_links.txt +0 -0
- {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass.egg-info/requires.txt +0 -0
- {hyperclass-0.0.4 → hyperclass-0.0.5}/hyperclass.egg-info/top_level.txt +0 -0
- {hyperclass-0.0.4 → hyperclass-0.0.5}/setup.cfg +0 -0
- {hyperclass-0.0.4 → hyperclass-0.0.5}/tests/test_cli.py +0 -0
- {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.
|
|
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.
|
|
243
|
-
|
|
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.
|
|
213
|
-
|
|
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:
|
|
@@ -173,33 +173,38 @@ def fragment(*children: Any) -> Fragment:
|
|
|
173
173
|
|
|
174
174
|
class RenderContext:
|
|
175
175
|
def __init__(self) -> None:
|
|
176
|
-
self.
|
|
177
|
-
self.
|
|
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.
|
|
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
|
|
185
|
+
for semantic_class in classes
|
|
186
|
+
for rule in semantic_class.__dict__.values()
|
|
185
187
|
):
|
|
186
|
-
self.
|
|
187
|
-
self.
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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.
|
|
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.
|
|
243
|
-
|
|
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:
|
|
@@ -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 "
|
|
70
|
+
assert '<style id="hyperclass-styles">' in document
|
|
71
71
|
assert (
|
|
72
|
-
".warning-card{
|
|
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}"
|
|
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
|
|
117
|
+
"@media (max-width:40rem){.card.interactive-card"
|
|
118
|
+
"{grid-template-columns:1fr}}"
|
|
112
119
|
in document
|
|
113
120
|
)
|
|
114
121
|
|
|
@@ -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
|
+
)
|
|
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
|