hyperclass 0.0.3__tar.gz → 0.0.4__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.3 → hyperclass-0.0.4}/PKG-INFO +57 -4
  2. {hyperclass-0.0.3 → hyperclass-0.0.4}/README.md +56 -3
  3. {hyperclass-0.0.3 → hyperclass-0.0.4}/hyperclass/__init__.py +1 -1
  4. {hyperclass-0.0.3 → hyperclass-0.0.4}/hyperclass/html.py +64 -8
  5. {hyperclass-0.0.3 → hyperclass-0.0.4}/hyperclass/htmx.py +1 -1
  6. {hyperclass-0.0.3 → hyperclass-0.0.4}/hyperclass/wsgi.py +7 -7
  7. {hyperclass-0.0.3 → hyperclass-0.0.4}/hyperclass.egg-info/PKG-INFO +57 -4
  8. {hyperclass-0.0.3 → hyperclass-0.0.4}/pyproject.toml +1 -1
  9. {hyperclass-0.0.3 → hyperclass-0.0.4}/tests/test_bookmarks.py +5 -0
  10. {hyperclass-0.0.3 → hyperclass-0.0.4}/tests/test_html.py +52 -0
  11. {hyperclass-0.0.3 → hyperclass-0.0.4}/tests/test_htmx.py +17 -1
  12. hyperclass-0.0.4/tests/test_package.py +5 -0
  13. {hyperclass-0.0.3 → hyperclass-0.0.4}/tests/test_wsgi.py +10 -0
  14. hyperclass-0.0.3/tests/test_package.py +0 -5
  15. {hyperclass-0.0.3 → hyperclass-0.0.4}/LICENSE +0 -0
  16. {hyperclass-0.0.3 → hyperclass-0.0.4}/hyperclass/__main__.py +0 -0
  17. {hyperclass-0.0.3 → hyperclass-0.0.4}/hyperclass/css.py +0 -0
  18. {hyperclass-0.0.3 → hyperclass-0.0.4}/hyperclass.egg-info/SOURCES.txt +0 -0
  19. {hyperclass-0.0.3 → hyperclass-0.0.4}/hyperclass.egg-info/dependency_links.txt +0 -0
  20. {hyperclass-0.0.3 → hyperclass-0.0.4}/hyperclass.egg-info/requires.txt +0 -0
  21. {hyperclass-0.0.3 → hyperclass-0.0.4}/hyperclass.egg-info/top_level.txt +0 -0
  22. {hyperclass-0.0.3 → hyperclass-0.0.4}/setup.cfg +0 -0
  23. {hyperclass-0.0.3 → hyperclass-0.0.4}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperclass
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: Build interactive web applications as Python class hierarchies.
5
5
  Author: Grant Jenks
6
6
  License-Expression: Apache-2.0
@@ -50,7 +50,7 @@ from dataclasses import dataclass
50
50
 
51
51
  from hyperclass import (
52
52
  App, button, css, div, form, get, grid, hx, input, outer_morph,
53
- post, rem,
53
+ name, post, rem,
54
54
  )
55
55
 
56
56
 
@@ -64,11 +64,17 @@ class card(div):
64
64
  )
65
65
 
66
66
 
67
+ class guest_name(input):
68
+ name = name.name
69
+ placeholder = "Your name"
70
+ required = True
71
+
72
+
67
73
  class guest_form(form):
68
74
  style = css(display=grid, gap=.75 * rem)
69
75
 
70
76
  def content(self):
71
- yield input(name="name", placeholder="Your name", required=True)
77
+ yield guest_name()
72
78
  yield button("Say hello", type="submit")
73
79
 
74
80
 
@@ -179,6 +185,39 @@ class greeting(card):
179
185
  Text and attribute values are escaped by default. `markup(...)` is the explicit
180
186
  escape hatch for trusted HTML.
181
187
 
188
+ ## HTML attributes inherit too
189
+
190
+ Non-private class values become default HTML attributes:
191
+
192
+ ~~~python
193
+ from hyperclass import a, input, name
194
+
195
+
196
+ class external_link(a):
197
+ target = "_blank"
198
+ rel = "noreferrer"
199
+
200
+
201
+ class url_field(input):
202
+ type = "url"
203
+ name = name.url
204
+ required = True
205
+ autocomplete = "url"
206
+ ~~~
207
+
208
+ The defaults follow the same base-to-derived order as styles. Subclasses and
209
+ multiple-inheritance mixins can override them. Attributes passed to an instance
210
+ win last:
211
+
212
+ ~~~python
213
+ external_link("Same tab", href="/", target="_self", rel=None)
214
+ ~~~
215
+
216
+ `None` and `False` suppress an inherited attribute. Underscores in Python names
217
+ become hyphens, so `aria_label` renders as `aria-label`. Boolean `True` renders
218
+ as a valueless HTML attribute. An `hx = hx.get(...)` class default expands into
219
+ the corresponding htmx attributes.
220
+
182
221
  ## CSS is Python too
183
222
 
184
223
  Base styles, pseudo-states, and media rules live on the component:
@@ -203,7 +242,7 @@ class primary_button(button):
203
242
  Pages collect only the rules used by their element tree. Python inheritance and
204
243
  the CSS cascade cooperate instead of imitating one another.
205
244
 
206
- ## Classes and IDs are selectors
245
+ ## Classes, IDs, and names are selectors
207
246
 
208
247
  Classes can be used directly anywhere a selector is expected:
209
248
 
@@ -226,6 +265,19 @@ assert id.unread_count is id.unread_count
226
265
  As an HTML attribute, `id.unread_count` renders as `unread-count`. As a
227
266
  selector, it renders as `#unread-count`.
228
267
 
268
+ Form names work the same way while preserving Python underscores:
269
+
270
+ ~~~python
271
+ from hyperclass import name
272
+
273
+ input(name=name.search_query)
274
+ request.form[name.search_query]
275
+ hx.get(search, include=name.search_query, target=id.results)
276
+ ~~~
277
+
278
+ As an attribute, `name.search_query` renders as `search_query`. As a selector,
279
+ it renders as `[name="search_query"]`. Repeated access returns the same object.
280
+
229
281
  ## Routes are references, not strings
230
282
 
231
283
  Application subclasses collect decorated method routes:
@@ -286,6 +338,7 @@ return a more specific `Response`.
286
338
 
287
339
  The underlying values remain available as `request.form`, `request.query`,
288
340
  `.get(...)`, `.getlist(...)`, and `.int(...)` when explicit parsing is clearer.
341
+ Those accessors accept first-class `name.*` objects as well as strings.
289
342
 
290
343
  ## WSGI and htmx 4
291
344
 
@@ -20,7 +20,7 @@ from dataclasses import dataclass
20
20
 
21
21
  from hyperclass import (
22
22
  App, button, css, div, form, get, grid, hx, input, outer_morph,
23
- post, rem,
23
+ name, post, rem,
24
24
  )
25
25
 
26
26
 
@@ -34,11 +34,17 @@ class card(div):
34
34
  )
35
35
 
36
36
 
37
+ class guest_name(input):
38
+ name = name.name
39
+ placeholder = "Your name"
40
+ required = True
41
+
42
+
37
43
  class guest_form(form):
38
44
  style = css(display=grid, gap=.75 * rem)
39
45
 
40
46
  def content(self):
41
- yield input(name="name", placeholder="Your name", required=True)
47
+ yield guest_name()
42
48
  yield button("Say hello", type="submit")
43
49
 
44
50
 
@@ -149,6 +155,39 @@ class greeting(card):
149
155
  Text and attribute values are escaped by default. `markup(...)` is the explicit
150
156
  escape hatch for trusted HTML.
151
157
 
158
+ ## HTML attributes inherit too
159
+
160
+ Non-private class values become default HTML attributes:
161
+
162
+ ~~~python
163
+ from hyperclass import a, input, name
164
+
165
+
166
+ class external_link(a):
167
+ target = "_blank"
168
+ rel = "noreferrer"
169
+
170
+
171
+ class url_field(input):
172
+ type = "url"
173
+ name = name.url
174
+ required = True
175
+ autocomplete = "url"
176
+ ~~~
177
+
178
+ The defaults follow the same base-to-derived order as styles. Subclasses and
179
+ multiple-inheritance mixins can override them. Attributes passed to an instance
180
+ win last:
181
+
182
+ ~~~python
183
+ external_link("Same tab", href="/", target="_self", rel=None)
184
+ ~~~
185
+
186
+ `None` and `False` suppress an inherited attribute. Underscores in Python names
187
+ become hyphens, so `aria_label` renders as `aria-label`. Boolean `True` renders
188
+ as a valueless HTML attribute. An `hx = hx.get(...)` class default expands into
189
+ the corresponding htmx attributes.
190
+
152
191
  ## CSS is Python too
153
192
 
154
193
  Base styles, pseudo-states, and media rules live on the component:
@@ -173,7 +212,7 @@ class primary_button(button):
173
212
  Pages collect only the rules used by their element tree. Python inheritance and
174
213
  the CSS cascade cooperate instead of imitating one another.
175
214
 
176
- ## Classes and IDs are selectors
215
+ ## Classes, IDs, and names are selectors
177
216
 
178
217
  Classes can be used directly anywhere a selector is expected:
179
218
 
@@ -196,6 +235,19 @@ assert id.unread_count is id.unread_count
196
235
  As an HTML attribute, `id.unread_count` renders as `unread-count`. As a
197
236
  selector, it renders as `#unread-count`.
198
237
 
238
+ Form names work the same way while preserving Python underscores:
239
+
240
+ ~~~python
241
+ from hyperclass import name
242
+
243
+ input(name=name.search_query)
244
+ request.form[name.search_query]
245
+ hx.get(search, include=name.search_query, target=id.results)
246
+ ~~~
247
+
248
+ As an attribute, `name.search_query` renders as `search_query`. As a selector,
249
+ it renders as `[name="search_query"]`. Repeated access returns the same object.
250
+
199
251
  ## Routes are references, not strings
200
252
 
201
253
  Application subclasses collect decorated method routes:
@@ -256,6 +308,7 @@ return a more specific `Response`.
256
308
 
257
309
  The underlying values remain available as `request.form`, `request.query`,
258
310
  `.get(...)`, `.getlist(...)`, and `.int(...)` when explicit parsing is clearer.
311
+ Those accessors accept first-class `name.*` objects as well as strings.
259
312
 
260
313
  ## WSGI and htmx 4
261
314
 
@@ -62,7 +62,7 @@ from .wsgi import (
62
62
  )
63
63
  from .wsgi import delete as delete_route
64
64
 
65
- __version__ = "0.0.3"
65
+ __version__ = "0.0.4"
66
66
 
67
67
  hidden = "hidden"
68
68
  submit = "submit"
@@ -7,7 +7,7 @@ from dataclasses import dataclass
7
7
  from html import escape
8
8
  from typing import Any
9
9
 
10
- from .css import Media, PSEUDO_STATES, Style
10
+ from .css import PSEUDO_STATES, Media, Style
11
11
 
12
12
  HTMX_SRC = "https://cdn.jsdelivr.net/npm/htmx.org@4.0.0"
13
13
  HTMX_INTEGRITY = (
@@ -51,6 +51,38 @@ class IdNamespace:
51
51
  id = IdNamespace()
52
52
 
53
53
 
54
+ @dataclass(frozen=True)
55
+ class Name:
56
+ """A first-class form name which is also usable as a selector."""
57
+
58
+ name: str
59
+
60
+ @property
61
+ def selector(self) -> str:
62
+ return f'[name="{self.name}"]'
63
+
64
+ def __str__(self) -> str:
65
+ return self.name
66
+
67
+
68
+ class NameNamespace:
69
+ def __init__(self) -> None:
70
+ self._values: dict[str, Name] = {}
71
+
72
+ def __getattr__(self, value: str) -> Name:
73
+ if value.startswith("_"):
74
+ raise AttributeError(value)
75
+ try:
76
+ return self._values[value]
77
+ except KeyError:
78
+ field_name = Name(value)
79
+ self._values[value] = field_name
80
+ return field_name
81
+
82
+
83
+ name = NameNamespace()
84
+
85
+
54
86
  class ElementMeta(type):
55
87
  @property
56
88
  def selector(cls) -> str:
@@ -93,12 +125,14 @@ def semantic_classes(value: type) -> tuple[type, ...]:
93
125
  def selector(value: Any) -> str:
94
126
  """Turn a selector string, element class, or element instance into CSS."""
95
127
 
96
- if isinstance(value, Id):
128
+ if isinstance(value, (Id, Name)):
97
129
  return value.selector
98
130
  if isinstance(value, str):
99
131
  return value
100
132
  if isinstance(value, element):
101
- identity = getattr(value, "_attrs", {}).get("id")
133
+ attributes = _class_attributes(type(value))
134
+ attributes.update(getattr(value, "_attrs", {}))
135
+ identity = attributes.get("id")
102
136
  if identity:
103
137
  return f"#{identity}"
104
138
  return selector(type(value))
@@ -183,6 +217,27 @@ def _attribute_value(value: Any) -> str:
183
217
  return str(value)
184
218
 
185
219
 
220
+ def _attribute(target: dict[str, Any], name: str, value: Any) -> None:
221
+ if name == "hx" and isinstance(value, Mapping):
222
+ target.update(value)
223
+ else:
224
+ target[_attribute_name(name)] = value
225
+
226
+
227
+ def _class_attributes(value: type) -> dict[str, Any]:
228
+ attributes: dict[str, Any] = {}
229
+ for semantic_class in semantic_classes(value):
230
+ for name, default in semantic_class.__dict__.items():
231
+ if name.startswith("_") or isinstance(default, (Style, Media, type)):
232
+ continue
233
+ if isinstance(default, (classmethod, staticmethod, property)):
234
+ continue
235
+ if callable(default):
236
+ continue
237
+ _attribute(attributes, name, default)
238
+ return attributes
239
+
240
+
186
241
  class element(metaclass=ElementMeta):
187
242
  """Base class for all rendered HTML elements."""
188
243
 
@@ -190,10 +245,7 @@ class element(metaclass=ElementMeta):
190
245
  self._children = children
191
246
  self._attrs: dict[str, Any] = {}
192
247
  for name, value in attributes.items():
193
- if name == "hx" and isinstance(value, Mapping):
194
- self._attrs.update(value)
195
- else:
196
- self._attrs[_attribute_name(name)] = value
248
+ _attribute(self._attrs, name, value)
197
249
 
198
250
  @property
199
251
  def selector(self) -> str:
@@ -256,7 +308,8 @@ partial = ElementMeta(
256
308
 
257
309
 
258
310
  def _render_attributes(node: element, classes: tuple[type, ...]) -> str:
259
- attributes = dict(getattr(node, "_attrs", {}))
311
+ attributes = _class_attributes(type(node))
312
+ attributes.update(getattr(node, "_attrs", {}))
260
313
  generated = [class_name(value) for value in classes]
261
314
  explicit = attributes.pop("class", attributes.pop("class-", None))
262
315
  if explicit:
@@ -365,12 +418,15 @@ __all__ = [
365
418
  "Id",
366
419
  "IdNamespace",
367
420
  "Markup",
421
+ "Name",
422
+ "NameNamespace",
368
423
  "Page",
369
424
  "class_name",
370
425
  "element",
371
426
  "fragment",
372
427
  "id",
373
428
  "markup",
429
+ "name",
374
430
  "page",
375
431
  "partial",
376
432
  "render",
@@ -66,7 +66,7 @@ class Htmx:
66
66
  for name, value in options.items():
67
67
  if value is None:
68
68
  continue
69
- if name in {"target", "select", "select_oob", "sync"}:
69
+ if name in {"include", "target", "select", "select_oob", "sync"}:
70
70
  value = selector(value) if not isinstance(value, Target) else value
71
71
  values[_option_name(name)] = value
72
72
  return Attributes(values)
@@ -230,8 +230,8 @@ class Values(Mapping[str, str]):
230
230
  def __init__(self, values: Mapping[str, list[str]] | None = None):
231
231
  self._values = dict(values or {})
232
232
 
233
- def __getitem__(self, key: str) -> str:
234
- return self._values[key][-1]
233
+ def __getitem__(self, key: Any) -> str:
234
+ return self._values[str(key)][-1]
235
235
 
236
236
  def __iter__(self) -> Iterator[str]:
237
237
  return iter(self._values)
@@ -239,14 +239,14 @@ class Values(Mapping[str, str]):
239
239
  def __len__(self) -> int:
240
240
  return len(self._values)
241
241
 
242
- def get(self, key: str, default: Any = None) -> Any:
243
- values = self._values.get(key)
242
+ def get(self, key: Any, default: Any = None) -> Any:
243
+ values = self._values.get(str(key))
244
244
  return values[-1] if values else default
245
245
 
246
- def getlist(self, key: str) -> list[str]:
247
- return list(self._values.get(key, ()))
246
+ def getlist(self, key: Any) -> list[str]:
247
+ return list(self._values.get(str(key), ()))
248
248
 
249
- def int(self, key: str, default: int | None = None) -> int:
249
+ def int(self, key: Any, default: int | None = None) -> int:
250
250
  value = self.get(key)
251
251
  if value is None:
252
252
  if default is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperclass
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: Build interactive web applications as Python class hierarchies.
5
5
  Author: Grant Jenks
6
6
  License-Expression: Apache-2.0
@@ -50,7 +50,7 @@ from dataclasses import dataclass
50
50
 
51
51
  from hyperclass import (
52
52
  App, button, css, div, form, get, grid, hx, input, outer_morph,
53
- post, rem,
53
+ name, post, rem,
54
54
  )
55
55
 
56
56
 
@@ -64,11 +64,17 @@ class card(div):
64
64
  )
65
65
 
66
66
 
67
+ class guest_name(input):
68
+ name = name.name
69
+ placeholder = "Your name"
70
+ required = True
71
+
72
+
67
73
  class guest_form(form):
68
74
  style = css(display=grid, gap=.75 * rem)
69
75
 
70
76
  def content(self):
71
- yield input(name="name", placeholder="Your name", required=True)
77
+ yield guest_name()
72
78
  yield button("Say hello", type="submit")
73
79
 
74
80
 
@@ -179,6 +185,39 @@ class greeting(card):
179
185
  Text and attribute values are escaped by default. `markup(...)` is the explicit
180
186
  escape hatch for trusted HTML.
181
187
 
188
+ ## HTML attributes inherit too
189
+
190
+ Non-private class values become default HTML attributes:
191
+
192
+ ~~~python
193
+ from hyperclass import a, input, name
194
+
195
+
196
+ class external_link(a):
197
+ target = "_blank"
198
+ rel = "noreferrer"
199
+
200
+
201
+ class url_field(input):
202
+ type = "url"
203
+ name = name.url
204
+ required = True
205
+ autocomplete = "url"
206
+ ~~~
207
+
208
+ The defaults follow the same base-to-derived order as styles. Subclasses and
209
+ multiple-inheritance mixins can override them. Attributes passed to an instance
210
+ win last:
211
+
212
+ ~~~python
213
+ external_link("Same tab", href="/", target="_self", rel=None)
214
+ ~~~
215
+
216
+ `None` and `False` suppress an inherited attribute. Underscores in Python names
217
+ become hyphens, so `aria_label` renders as `aria-label`. Boolean `True` renders
218
+ as a valueless HTML attribute. An `hx = hx.get(...)` class default expands into
219
+ the corresponding htmx attributes.
220
+
182
221
  ## CSS is Python too
183
222
 
184
223
  Base styles, pseudo-states, and media rules live on the component:
@@ -203,7 +242,7 @@ class primary_button(button):
203
242
  Pages collect only the rules used by their element tree. Python inheritance and
204
243
  the CSS cascade cooperate instead of imitating one another.
205
244
 
206
- ## Classes and IDs are selectors
245
+ ## Classes, IDs, and names are selectors
207
246
 
208
247
  Classes can be used directly anywhere a selector is expected:
209
248
 
@@ -226,6 +265,19 @@ assert id.unread_count is id.unread_count
226
265
  As an HTML attribute, `id.unread_count` renders as `unread-count`. As a
227
266
  selector, it renders as `#unread-count`.
228
267
 
268
+ Form names work the same way while preserving Python underscores:
269
+
270
+ ~~~python
271
+ from hyperclass import name
272
+
273
+ input(name=name.search_query)
274
+ request.form[name.search_query]
275
+ hx.get(search, include=name.search_query, target=id.results)
276
+ ~~~
277
+
278
+ As an attribute, `name.search_query` renders as `search_query`. As a selector,
279
+ it renders as `[name="search_query"]`. Repeated access returns the same object.
280
+
229
281
  ## Routes are references, not strings
230
282
 
231
283
  Application subclasses collect decorated method routes:
@@ -286,6 +338,7 @@ return a more specific `Response`.
286
338
 
287
339
  The underlying values remain available as `request.form`, `request.query`,
288
340
  `.get(...)`, `.getlist(...)`, and `.int(...)` when explicit parsing is clearer.
341
+ Those accessors accept first-class `name.*` objects as well as strings.
289
342
 
290
343
  ## WSGI and htmx 4
291
344
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperclass"
3
- version = "0.0.3"
3
+ version = "0.0.4"
4
4
  description = "Build interactive web applications as Python class hierarchies."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -55,6 +55,11 @@ def test_bookmark_lifecycle(tmp_path):
55
55
  assert "<!doctype html>" not in payload
56
56
  assert "Read this" in payload
57
57
  assert "1 unread bookmark" in payload
58
+ assert (
59
+ '<input class="text-field url-field" type="url" name="url" required'
60
+ in payload
61
+ )
62
+ assert 'target="_blank" rel="noreferrer"' in payload
58
63
  assert 'hx-patch="/bookmarks/1"' in payload
59
64
  bookmark = app.store.list()[0]
60
65
 
@@ -1,11 +1,15 @@
1
1
  from hyperclass import (
2
2
  Page,
3
+ a,
4
+ button,
3
5
  css,
4
6
  div,
5
7
  fragment,
6
8
  grid,
7
9
  id,
10
+ input,
8
11
  media,
12
+ name,
9
13
  orange,
10
14
  outer_morph,
11
15
  partial,
@@ -107,3 +111,51 @@ def test_page_collects_state_and_media_styles_from_classes():
107
111
  "@media (max-width:40rem){.interactive-card{grid-template-columns:1fr}}"
108
112
  in document
109
113
  )
114
+
115
+
116
+ def test_html_attributes_are_inherited_and_can_be_overridden():
117
+ class external_link(a):
118
+ target = "_blank"
119
+ rel = "noreferrer"
120
+
121
+ assert render(external_link("Python", href="https://python.org")) == (
122
+ '<a class="external-link" target="_blank" rel="noreferrer" '
123
+ 'href="https://python.org">Python</a>'
124
+ )
125
+ assert render(external_link("Here", href="/", target="_self", rel=None)) == (
126
+ '<a class="external-link" target="_self" href="/">Here</a>'
127
+ )
128
+
129
+
130
+ def test_multiple_inheritance_composes_attribute_defaults():
131
+ class disabled:
132
+ disabled = True
133
+ aria_disabled = "true"
134
+
135
+ class caution:
136
+ title = "Careful"
137
+
138
+ class cautious_button(button, disabled, caution):
139
+ title = "Really careful"
140
+
141
+ assert render(cautious_button("Continue")) == (
142
+ '<button class="disabled caution cautious-button" disabled '
143
+ 'aria-disabled="true" title="Really careful">Continue</button>'
144
+ )
145
+
146
+
147
+ def test_names_are_lazy_interned_attributes_and_selectors():
148
+ assert name.search_query is name.search_query
149
+ assert str(name.search_query) == "search_query"
150
+ assert name.search_query.selector == '[name="search_query"]'
151
+ assert selector(name.search_query) == '[name="search_query"]'
152
+ assert render(input(name=name.search_query)) == (
153
+ '<input name="search_query">'
154
+ )
155
+
156
+
157
+ def test_class_default_id_controls_instance_selector():
158
+ class results(div):
159
+ id = id.search_results
160
+
161
+ assert results().selector == "#search-results"
@@ -1,4 +1,4 @@
1
- from hyperclass import closest, div, hx, outer_morph, render
1
+ from hyperclass import closest, div, hx, id, name, outer_morph, render
2
2
 
3
3
 
4
4
  class counter(div):
@@ -24,3 +24,19 @@ def test_plain_class_target_becomes_a_selector():
24
24
  assert render(div(hx=hx.get("/counter", target=counter))) == (
25
25
  '<div hx-get="/counter" hx-target=".counter"></div>'
26
26
  )
27
+
28
+
29
+ def test_class_attributes_can_declare_htmx_and_name_selectors():
30
+ class live_search(div):
31
+ hx = hx.get(
32
+ "/search",
33
+ include=name.query,
34
+ target=id.results,
35
+ swap=outer_morph,
36
+ )
37
+
38
+ assert render(live_search()) == (
39
+ '<div class="live-search" hx-get="/search" '
40
+ 'hx-include="[name=&quot;query&quot;]" hx-target="#results" '
41
+ 'hx-swap="outerMorph"></div>'
42
+ )
@@ -0,0 +1,5 @@
1
+ import hyperclass
2
+
3
+
4
+ def test_version():
5
+ assert hyperclass.__version__ == "0.0.4"
@@ -5,6 +5,7 @@ from wsgiref.util import setup_testing_defaults
5
5
 
6
6
  from hyperclass import (
7
7
  App,
8
+ Values,
8
9
  button,
9
10
  closest,
10
11
  div,
@@ -13,6 +14,7 @@ from hyperclass import (
13
14
  hidden,
14
15
  hx,
15
16
  input,
17
+ name,
16
18
  outer_morph,
17
19
  output,
18
20
  patch,
@@ -266,3 +268,11 @@ def test_dataclass_binding_uses_defaults_and_reports_bad_values():
266
268
  )
267
269
  assert captured["status"] == "400 Bad Request"
268
270
  assert payload == "invalid form value for priority: 'high'"
271
+
272
+
273
+ def test_values_accept_first_class_form_names():
274
+ values = Values({"search_query": ["python", "wsgi"], "page": ["3"]})
275
+ assert values[name.search_query] == "wsgi"
276
+ assert values.get(name.search_query) == "wsgi"
277
+ assert values.getlist(name.search_query) == ["python", "wsgi"]
278
+ assert values.int(name.page) == 3
@@ -1,5 +0,0 @@
1
- import hyperclass
2
-
3
-
4
- def test_version():
5
- assert hyperclass.__version__ == "0.0.3"
File without changes
File without changes
File without changes
File without changes