hyperclass 0.0.2__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 (26) hide show
  1. hyperclass-0.0.4/PKG-INFO +401 -0
  2. hyperclass-0.0.4/README.md +371 -0
  3. {hyperclass-0.0.2 → hyperclass-0.0.4}/hyperclass/__init__.py +1 -1
  4. hyperclass-0.0.4/hyperclass/__main__.py +37 -0
  5. {hyperclass-0.0.2 → hyperclass-0.0.4}/hyperclass/html.py +64 -8
  6. {hyperclass-0.0.2 → hyperclass-0.0.4}/hyperclass/htmx.py +1 -1
  7. {hyperclass-0.0.2 → hyperclass-0.0.4}/hyperclass/wsgi.py +97 -13
  8. hyperclass-0.0.4/hyperclass.egg-info/PKG-INFO +401 -0
  9. {hyperclass-0.0.2 → hyperclass-0.0.4}/hyperclass.egg-info/SOURCES.txt +2 -0
  10. {hyperclass-0.0.2 → hyperclass-0.0.4}/pyproject.toml +1 -1
  11. {hyperclass-0.0.2 → hyperclass-0.0.4}/tests/test_bookmarks.py +69 -3
  12. hyperclass-0.0.4/tests/test_cli.py +25 -0
  13. {hyperclass-0.0.2 → hyperclass-0.0.4}/tests/test_html.py +52 -0
  14. {hyperclass-0.0.2 → hyperclass-0.0.4}/tests/test_htmx.py +17 -1
  15. hyperclass-0.0.4/tests/test_package.py +5 -0
  16. {hyperclass-0.0.2 → hyperclass-0.0.4}/tests/test_wsgi.py +76 -0
  17. hyperclass-0.0.2/PKG-INFO +0 -283
  18. hyperclass-0.0.2/README.md +0 -253
  19. hyperclass-0.0.2/hyperclass.egg-info/PKG-INFO +0 -283
  20. hyperclass-0.0.2/tests/test_package.py +0 -5
  21. {hyperclass-0.0.2 → hyperclass-0.0.4}/LICENSE +0 -0
  22. {hyperclass-0.0.2 → hyperclass-0.0.4}/hyperclass/css.py +0 -0
  23. {hyperclass-0.0.2 → hyperclass-0.0.4}/hyperclass.egg-info/dependency_links.txt +0 -0
  24. {hyperclass-0.0.2 → hyperclass-0.0.4}/hyperclass.egg-info/requires.txt +0 -0
  25. {hyperclass-0.0.2 → hyperclass-0.0.4}/hyperclass.egg-info/top_level.txt +0 -0
  26. {hyperclass-0.0.2 → hyperclass-0.0.4}/setup.cfg +0 -0
@@ -0,0 +1,401 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperclass
3
+ Version: 0.0.4
4
+ Summary: Build interactive web applications as Python class hierarchies.
5
+ Author: Grant Jenks
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/grantjenks/python-hyperclass
8
+ Project-URL: Issue Tracker, https://github.com/grantjenks/python-hyperclass/issues
9
+ Project-URL: Source Code, https://github.com/grantjenks/python-hyperclass
10
+ Keywords: html,htmx,css,wsgi,web
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: nox>=2024.4.15; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # Hyperclass
32
+
33
+ > **Subclass the web.**
34
+
35
+ Hyperclass is a small experiment in building interactive web applications as
36
+ Python class hierarchies.
37
+
38
+ HTML elements are Python base classes. Python subclasses become CSS classes.
39
+ Styles follow inheritance. Routes are methods. Decorated handlers are URLs.
40
+ Interaction is ordinary HTTP over WSGI, with htmx 4 in the browser.
41
+
42
+ ~~~console
43
+ pip install hyperclass
44
+ ~~~
45
+
46
+ ## Sixty-second tour
47
+
48
+ ~~~python
49
+ from dataclasses import dataclass
50
+
51
+ from hyperclass import (
52
+ App, button, css, div, form, get, grid, hx, input, outer_morph,
53
+ name, post, rem,
54
+ )
55
+
56
+
57
+ class card(div):
58
+ style = css(
59
+ display=grid,
60
+ gap=1 * rem,
61
+ padding=1.25 * rem,
62
+ border="1px solid #ddd",
63
+ border_radius=.75 * rem,
64
+ )
65
+
66
+
67
+ class guest_name(input):
68
+ name = name.name
69
+ placeholder = "Your name"
70
+ required = True
71
+
72
+
73
+ class guest_form(form):
74
+ style = css(display=grid, gap=.75 * rem)
75
+
76
+ def content(self):
77
+ yield guest_name()
78
+ yield button("Say hello", type="submit")
79
+
80
+
81
+ @dataclass
82
+ class Guest:
83
+ name: str
84
+
85
+
86
+ class guestbook(App):
87
+ @get("/")
88
+ def index(self, request):
89
+ return card(
90
+ "Who are you?",
91
+ guest_form(
92
+ hx=hx.post(
93
+ guestbook.create,
94
+ target=card,
95
+ swap=outer_morph,
96
+ )
97
+ ),
98
+ )
99
+
100
+ @post("/guests")
101
+ def create(self, request, form: Guest):
102
+ return card(f"Hello, {form.name}!")
103
+
104
+
105
+ app = guestbook(title="Guestbook")
106
+ ~~~
107
+
108
+ Run it:
109
+
110
+ ~~~console
111
+ python -m hyperclass myapp:app
112
+ ~~~
113
+
114
+ Then open <http://127.0.0.1:8000>. There is no JavaScript build, template
115
+ language, ASGI dependency, or CSS file hidden elsewhere.
116
+
117
+ ## HTML classes are Python classes
118
+
119
+ Every built-in element can be subclassed:
120
+
121
+ ~~~python
122
+ from hyperclass import css, div, grid, orange, rem
123
+
124
+
125
+ class card(div):
126
+ style = css(display=grid, gap=1 * rem, padding=1.25 * rem)
127
+
128
+
129
+ class warning_card(card):
130
+ style = css(
131
+ border_color=orange,
132
+ background=orange.fade(0.08),
133
+ )
134
+ ~~~
135
+
136
+ Calling:
137
+
138
+ ~~~python
139
+ warning_card("Something happened")
140
+ ~~~
141
+
142
+ produces ordinary, inspectable HTML:
143
+
144
+ ~~~html
145
+ <div class="card warning-card">Something happened</div>
146
+ ~~~
147
+
148
+ The first built-in HTML ancestor determines the tag. Each semantic subclass
149
+ contributes a CSS class. `snake_case` becomes `kebab-case`.
150
+
151
+ Multiple inheritance composes behavior and styles:
152
+
153
+ ~~~python
154
+ class compact:
155
+ style = css(padding=.5 * rem)
156
+
157
+
158
+ class clickable:
159
+ style = css(cursor="pointer")
160
+
161
+
162
+ class result_card(card, compact, clickable):
163
+ pass
164
+ ~~~
165
+
166
+ ~~~html
167
+ <div class="card compact clickable result-card"></div>
168
+ ~~~
169
+
170
+ Components use normal Python state and methods:
171
+
172
+ ~~~python
173
+ from hyperclass import strong
174
+
175
+
176
+ class greeting(card):
177
+ def __init__(self, name):
178
+ self.name = name
179
+
180
+ def content(self):
181
+ yield "Hello, "
182
+ yield strong(self.name)
183
+ ~~~
184
+
185
+ Text and attribute values are escaped by default. `markup(...)` is the explicit
186
+ escape hatch for trusted HTML.
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
+
221
+ ## CSS is Python too
222
+
223
+ Base styles, pseudo-states, and media rules live on the component:
224
+
225
+ ~~~python
226
+ from hyperclass import button, css, media, rem
227
+
228
+
229
+ class primary_button(button):
230
+ style = css(
231
+ padding=".7rem 1rem",
232
+ background="#6d28d9",
233
+ color="white",
234
+ border=0,
235
+ border_radius=.5 * rem,
236
+ )
237
+ hover = css(background="#5b21b6")
238
+ focus_visible = css(outline="3px solid #c4b5fd")
239
+ narrow = media(max_width=40 * rem, width="100%")
240
+ ~~~
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.
244
+
245
+ ## Classes, IDs, and names are selectors
246
+
247
+ Classes can be used directly anywhere a selector is expected:
248
+
249
+ ~~~python
250
+ hx.get(search, target=result_card)
251
+ closest(card)
252
+ ~~~
253
+
254
+ IDs are lazy, interned Python objects:
255
+
256
+ ~~~python
257
+ from hyperclass import id, span
258
+
259
+ span("3 unread", id=id.unread_count)
260
+ hx.get(count, target=id.unread_count)
261
+
262
+ assert id.unread_count is id.unread_count
263
+ ~~~
264
+
265
+ As an HTML attribute, `id.unread_count` renders as `unread-count`. As a
266
+ selector, it renders as `#unread-count`.
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
+
281
+ ## Routes are references, not strings
282
+
283
+ Application subclasses collect decorated method routes:
284
+
285
+ ~~~python
286
+ from hyperclass import App, get, patch
287
+
288
+
289
+ class bookmarks(App):
290
+ @get("/")
291
+ def index(self, request):
292
+ return bookmark_list(...)
293
+
294
+ @patch("/bookmarks/<int:bookmark_id>")
295
+ def toggle(self, request, bookmark_id):
296
+ return bookmark_card(...)
297
+ ~~~
298
+
299
+ Decorated handlers retain their route metadata:
300
+
301
+ ~~~python
302
+ bookmarks.toggle.url(bookmark_id=42)
303
+ # '/bookmarks/42'
304
+
305
+ hx.patch(
306
+ bookmarks.toggle,
307
+ bookmark_id=42,
308
+ target=bookmark_card,
309
+ )
310
+ ~~~
311
+
312
+ Typed path parameters are converted before the handler runs. Query strings can
313
+ be attached with `.url(query={...})` or the `query=` option on an htmx request.
314
+
315
+ ## Forms bind to dataclasses
316
+
317
+ Annotate a route parameter with a dataclass and Hyperclass builds it from the
318
+ submitted form:
319
+
320
+ ~~~python
321
+ @dataclass
322
+ class NewBookmark:
323
+ url: str
324
+ title: str = ""
325
+
326
+
327
+ class bookmarks(App):
328
+ @post("/bookmarks")
329
+ def create(self, request, form: NewBookmark):
330
+ self.store.add(form.url, form.title)
331
+ return bookmark_list(...)
332
+ ~~~
333
+
334
+ Binding supports strings, integers, floats, booleans, optional values, and
335
+ lists or tuples of those values. Dataclass defaults remain defaults. Invalid or
336
+ missing required values produce a `400 Bad Request`; application validation can
337
+ return a more specific `Response`.
338
+
339
+ The underlying values remain available as `request.form`, `request.query`,
340
+ `.get(...)`, `.getlist(...)`, and `.int(...)` when explicit parsing is clearer.
341
+ Those accessors accept first-class `name.*` objects as well as strings.
342
+
343
+ ## WSGI and htmx 4
344
+
345
+ A Hyperclass application is a normal WSGI callable. Use the standard-library
346
+ development server:
347
+
348
+ ~~~console
349
+ python -m hyperclass package.module:app
350
+ python -m hyperclass package.module:app --host 0.0.0.0 --port 9000
351
+ ~~~
352
+
353
+ Production can use any WSGI server. Returning an element from an ordinary
354
+ browser request wraps it in a complete page. Returning the same element to an
355
+ htmx request sends only the fragment to swap.
356
+
357
+ `Page(...)` controls the document explicitly. Pages include a pinned htmx 4
358
+ asset from jsDelivr. htmx 4 `<hx-partial>` responses can update several
359
+ object-selected regions from one request.
360
+
361
+ ## Try the examples
362
+
363
+ Clone the repository and run the persistent SQLite bookmark inbox:
364
+
365
+ ~~~console
366
+ git clone https://github.com/grantjenks/python-hyperclass
367
+ cd python-hyperclass
368
+ python -m hyperclass examples.bookmarks:app
369
+ ~~~
370
+
371
+ The bookmark app adds, searches, filters, edits, marks, and deletes bookmarks.
372
+ Its implementation is Python plus SQLite, WSGI, generated CSS, and htmx. It is
373
+ also a compact integration test for the framework's ideas.
374
+
375
+ For the smallest example:
376
+
377
+ ~~~console
378
+ python -m hyperclass examples.counter:app
379
+ ~~~
380
+
381
+ ## Principles
382
+
383
+ - **Python is the authoring language.** Control flow, composition, inheritance,
384
+ validation, and reuse are ordinary Python.
385
+ - **The browser remains the browser.** Hyperclass emits standard HTML and CSS
386
+ rather than recreating the DOM on the server.
387
+ - **Classes mean classes.** Python inheritance has a visible relationship to
388
+ HTML classes and the CSS cascade.
389
+ - **HTTP is the state boundary.** There is no hydration protocol or hidden
390
+ client component lifecycle.
391
+ - **Output should be boring.** Generated markup stays readable in View Source
392
+ and DevTools.
393
+ - **Small is a feature.** Prefer the standard library, WSGI, and a pinned htmx
394
+ asset over a framework stack.
395
+
396
+ ## Status
397
+
398
+ Hyperclass is deliberately pre-alpha: useful enough to build small applications
399
+ and young enough for its API to change. Python 3.10 through 3.14 are tested.
400
+
401
+ Apache-2.0 licensed.