pywebview-htmx 0.3.1__tar.gz → 0.4.0__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.
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/PKG-INFO +37 -11
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/README.md +36 -10
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/pyproject.toml +22 -19
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/src/pywebview_htmx/static/runtime.js +42 -18
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/src/pywebview_htmx/static/themes/base.css +44 -102
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/src/pywebview_htmx/static/themes/cybermind.css +4 -13
- pywebview_htmx-0.4.0/tests/test_js_runtime.py +642 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/tests/test_repo_legibility.py +1 -1
- pywebview_htmx-0.3.1/tests/js_runtime_behavior_harness.mjs +0 -369
- pywebview_htmx-0.3.1/tests/test_js_contract_config_and_binding.py +0 -76
- pywebview_htmx-0.3.1/tests/test_js_contract_ergonomics.py +0 -125
- pywebview_htmx-0.3.1/tests/test_js_contract_request_and_swap.py +0 -96
- pywebview_htmx-0.3.1/tests/test_js_runtime_behavior.py +0 -27
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/LICENSE +0 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/src/pywebview_htmx/__init__.py +0 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/src/pywebview_htmx/py.typed +0 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/src/pywebview_htmx/runtime.py +0 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/src/pywebview_htmx/static/themes/aurora.css +0 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/src/pywebview_htmx/static/themes/paper.css +0 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/tests/conftest.py +0 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/tests/test_python_create_window.py +0 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/tests/test_python_injection.py +0 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/tests/test_python_params_attr.py +0 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/tests/test_python_script_loading.py +0 -0
- {pywebview_htmx-0.3.1 → pywebview_htmx-0.4.0}/tests/test_python_themes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pywebview-htmx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: HTMX-style declarative UI bindings for PyWebview apps
|
|
5
5
|
Author-Email: "B.T. Franklin" <brandon.franklin@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -120,12 +120,14 @@ The skill includes:
|
|
|
120
120
|
## Quick Start
|
|
121
121
|
|
|
122
122
|
```python
|
|
123
|
+
from html import escape
|
|
124
|
+
|
|
123
125
|
from pywebview_htmx import create_window
|
|
124
126
|
|
|
125
127
|
|
|
126
128
|
class API:
|
|
127
129
|
def greeting(self, params: dict) -> str:
|
|
128
|
-
name = params.get("name", "world")
|
|
130
|
+
name = escape(str(params.get("name", "world")))
|
|
129
131
|
return f"<p>Hello, {name}!</p>"
|
|
130
132
|
|
|
131
133
|
|
|
@@ -229,6 +231,12 @@ DOM event name to bind. Default is `click`.
|
|
|
229
231
|
<div py-call="show_tip" py-trigger="mouseenter">...</div>
|
|
230
232
|
```
|
|
231
233
|
|
|
234
|
+
When the runtime handles a native action that would leave or destructively
|
|
235
|
+
change the current document, it prevents that default action. This includes
|
|
236
|
+
form submission, link navigation, and submit/reset controls associated with a
|
|
237
|
+
form. Ordinary buttons and the native state changes of checkboxes, radio
|
|
238
|
+
buttons, and labels are preserved.
|
|
239
|
+
|
|
232
240
|
### `data-py-params` (optional)
|
|
233
241
|
|
|
234
242
|
JSON payload passed to Python method.
|
|
@@ -261,6 +269,8 @@ Unknown value falls back to `innerHTML`.
|
|
|
261
269
|
|
|
262
270
|
CSS selector of element receiving `.py-waiting` while request is in flight.
|
|
263
271
|
If missing, empty, invalid, or unresolvable, the triggering element is used.
|
|
272
|
+
The selector is resolved when each accepted request starts, so replacement
|
|
273
|
+
markup can provide the wait target for later requests.
|
|
264
274
|
|
|
265
275
|
### `py-policy` (optional)
|
|
266
276
|
|
|
@@ -279,6 +289,11 @@ Per-element concurrency override. If omitted, the global
|
|
|
279
289
|
- `settleDelay` (ms, default: `20`)
|
|
280
290
|
- `requestPolicy` (`"latest-wins"` default, or `"drop"`)
|
|
281
291
|
|
|
292
|
+
Runtime defaults are read for each accepted request. An element's explicit
|
|
293
|
+
`py-swap` continues to override `defaultSwapStyle`; otherwise, changing
|
|
294
|
+
`defaultSwapStyle` affects its next request even when the element is already
|
|
295
|
+
bound.
|
|
296
|
+
|
|
282
297
|
Example:
|
|
283
298
|
|
|
284
299
|
```js
|
|
@@ -297,21 +312,35 @@ window.pywebviewHtmx.config.settleDelay = 50;
|
|
|
297
312
|
- `py:ignored` (when `requestPolicy="drop"` and request is in flight)
|
|
298
313
|
- `py:error`
|
|
299
314
|
|
|
315
|
+
These lifecycle events are observational and non-cancelable. Calling
|
|
316
|
+
`preventDefault()` on them does not change request or swap behavior. Every
|
|
317
|
+
`py:error` event includes `event.detail.stale`, a boolean indicating whether a
|
|
318
|
+
newer request had already been issued in the same request scope when the error
|
|
319
|
+
was emitted. Errors from stale requests remain observable even though stale
|
|
320
|
+
successful responses do not update the DOM.
|
|
321
|
+
|
|
300
322
|
Example:
|
|
301
323
|
|
|
302
324
|
```js
|
|
303
325
|
document.body.addEventListener("py:error", (event) => {
|
|
304
|
-
console.error("pywebview-htmx error",
|
|
326
|
+
console.error("pywebview-htmx error", {
|
|
327
|
+
error: event.detail.error,
|
|
328
|
+
stale: event.detail.stale,
|
|
329
|
+
});
|
|
305
330
|
});
|
|
306
331
|
```
|
|
307
332
|
|
|
308
333
|
## Concurrency Behavior
|
|
309
334
|
|
|
310
|
-
`pywebview-htmx`
|
|
311
|
-
|
|
335
|
+
For a nonempty `py-target`, `pywebview-htmx` scopes request state by its
|
|
336
|
+
normalized selector string. Controls with the same selector coordinate request
|
|
337
|
+
ordering and `drop` behavior even when a swap replaces the matching DOM node.
|
|
338
|
+
Selector state is discarded after its in-flight requests finish. Controls
|
|
312
339
|
without `py-target` use the triggering element as their request state scope.
|
|
313
|
-
|
|
314
|
-
|
|
340
|
+
|
|
341
|
+
The `py-wait` selector is resolved for each accepted request. Loading state is
|
|
342
|
+
counted separately per resolved wait element, so a shared spinner stays active
|
|
343
|
+
until every request using that particular element finishes.
|
|
315
344
|
|
|
316
345
|
### `latest-wins` (default)
|
|
317
346
|
|
|
@@ -346,10 +375,7 @@ Set `theme=None` to disable automatic theme injection.
|
|
|
346
375
|
|
|
347
376
|
### Built-in Component Classes
|
|
348
377
|
|
|
349
|
-
The bundled CSS
|
|
350
|
-
the demo (`.hero`, `.demo-card`, `.btn`, etc.).
|
|
351
|
-
|
|
352
|
-
Recommended canonical classes:
|
|
378
|
+
The bundled CSS exposes canonical `pyh-*` component classes:
|
|
353
379
|
|
|
354
380
|
- Layout: `.pyh-shell`, `.pyh-hero`, `.pyh-grid`, `.pyh-card`
|
|
355
381
|
- Controls: `.pyh-btn`, `.pyh-btn-primary`, `.pyh-btn-secondary`, `.pyh-btn-ghost`, `.pyh-btn-danger`
|
|
@@ -80,12 +80,14 @@ The skill includes:
|
|
|
80
80
|
## Quick Start
|
|
81
81
|
|
|
82
82
|
```python
|
|
83
|
+
from html import escape
|
|
84
|
+
|
|
83
85
|
from pywebview_htmx import create_window
|
|
84
86
|
|
|
85
87
|
|
|
86
88
|
class API:
|
|
87
89
|
def greeting(self, params: dict) -> str:
|
|
88
|
-
name = params.get("name", "world")
|
|
90
|
+
name = escape(str(params.get("name", "world")))
|
|
89
91
|
return f"<p>Hello, {name}!</p>"
|
|
90
92
|
|
|
91
93
|
|
|
@@ -189,6 +191,12 @@ DOM event name to bind. Default is `click`.
|
|
|
189
191
|
<div py-call="show_tip" py-trigger="mouseenter">...</div>
|
|
190
192
|
```
|
|
191
193
|
|
|
194
|
+
When the runtime handles a native action that would leave or destructively
|
|
195
|
+
change the current document, it prevents that default action. This includes
|
|
196
|
+
form submission, link navigation, and submit/reset controls associated with a
|
|
197
|
+
form. Ordinary buttons and the native state changes of checkboxes, radio
|
|
198
|
+
buttons, and labels are preserved.
|
|
199
|
+
|
|
192
200
|
### `data-py-params` (optional)
|
|
193
201
|
|
|
194
202
|
JSON payload passed to Python method.
|
|
@@ -221,6 +229,8 @@ Unknown value falls back to `innerHTML`.
|
|
|
221
229
|
|
|
222
230
|
CSS selector of element receiving `.py-waiting` while request is in flight.
|
|
223
231
|
If missing, empty, invalid, or unresolvable, the triggering element is used.
|
|
232
|
+
The selector is resolved when each accepted request starts, so replacement
|
|
233
|
+
markup can provide the wait target for later requests.
|
|
224
234
|
|
|
225
235
|
### `py-policy` (optional)
|
|
226
236
|
|
|
@@ -239,6 +249,11 @@ Per-element concurrency override. If omitted, the global
|
|
|
239
249
|
- `settleDelay` (ms, default: `20`)
|
|
240
250
|
- `requestPolicy` (`"latest-wins"` default, or `"drop"`)
|
|
241
251
|
|
|
252
|
+
Runtime defaults are read for each accepted request. An element's explicit
|
|
253
|
+
`py-swap` continues to override `defaultSwapStyle`; otherwise, changing
|
|
254
|
+
`defaultSwapStyle` affects its next request even when the element is already
|
|
255
|
+
bound.
|
|
256
|
+
|
|
242
257
|
Example:
|
|
243
258
|
|
|
244
259
|
```js
|
|
@@ -257,21 +272,35 @@ window.pywebviewHtmx.config.settleDelay = 50;
|
|
|
257
272
|
- `py:ignored` (when `requestPolicy="drop"` and request is in flight)
|
|
258
273
|
- `py:error`
|
|
259
274
|
|
|
275
|
+
These lifecycle events are observational and non-cancelable. Calling
|
|
276
|
+
`preventDefault()` on them does not change request or swap behavior. Every
|
|
277
|
+
`py:error` event includes `event.detail.stale`, a boolean indicating whether a
|
|
278
|
+
newer request had already been issued in the same request scope when the error
|
|
279
|
+
was emitted. Errors from stale requests remain observable even though stale
|
|
280
|
+
successful responses do not update the DOM.
|
|
281
|
+
|
|
260
282
|
Example:
|
|
261
283
|
|
|
262
284
|
```js
|
|
263
285
|
document.body.addEventListener("py:error", (event) => {
|
|
264
|
-
console.error("pywebview-htmx error",
|
|
286
|
+
console.error("pywebview-htmx error", {
|
|
287
|
+
error: event.detail.error,
|
|
288
|
+
stale: event.detail.stale,
|
|
289
|
+
});
|
|
265
290
|
});
|
|
266
291
|
```
|
|
267
292
|
|
|
268
293
|
## Concurrency Behavior
|
|
269
294
|
|
|
270
|
-
`pywebview-htmx`
|
|
271
|
-
|
|
295
|
+
For a nonempty `py-target`, `pywebview-htmx` scopes request state by its
|
|
296
|
+
normalized selector string. Controls with the same selector coordinate request
|
|
297
|
+
ordering and `drop` behavior even when a swap replaces the matching DOM node.
|
|
298
|
+
Selector state is discarded after its in-flight requests finish. Controls
|
|
272
299
|
without `py-target` use the triggering element as their request state scope.
|
|
273
|
-
|
|
274
|
-
|
|
300
|
+
|
|
301
|
+
The `py-wait` selector is resolved for each accepted request. Loading state is
|
|
302
|
+
counted separately per resolved wait element, so a shared spinner stays active
|
|
303
|
+
until every request using that particular element finishes.
|
|
275
304
|
|
|
276
305
|
### `latest-wins` (default)
|
|
277
306
|
|
|
@@ -306,10 +335,7 @@ Set `theme=None` to disable automatic theme injection.
|
|
|
306
335
|
|
|
307
336
|
### Built-in Component Classes
|
|
308
337
|
|
|
309
|
-
The bundled CSS
|
|
310
|
-
the demo (`.hero`, `.demo-card`, `.btn`, etc.).
|
|
311
|
-
|
|
312
|
-
Recommended canonical classes:
|
|
338
|
+
The bundled CSS exposes canonical `pyh-*` component classes:
|
|
313
339
|
|
|
314
340
|
- Layout: `.pyh-shell`, `.pyh-hero`, `.pyh-grid`, `.pyh-card`
|
|
315
341
|
- Controls: `.pyh-btn`, `.pyh-btn-primary`, `.pyh-btn-secondary`, `.pyh-btn-ghost`, `.pyh-btn-danger`
|
|
@@ -21,7 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
22
|
"Topic :: Internet :: WWW/HTTP",
|
|
23
23
|
]
|
|
24
|
-
version = "0.
|
|
24
|
+
version = "0.4.0"
|
|
25
25
|
|
|
26
26
|
[project.license]
|
|
27
27
|
file = "LICENSE"
|
|
@@ -32,22 +32,14 @@ requires = [
|
|
|
32
32
|
]
|
|
33
33
|
build-backend = "pdm.backend"
|
|
34
34
|
|
|
35
|
-
[
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[tool.pdm.build]
|
|
42
|
-
package-dir = "src"
|
|
43
|
-
includes = [
|
|
44
|
-
"src/pywebview_htmx",
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=9.1.1",
|
|
38
|
+
"ruff>=0.15.19",
|
|
39
|
+
"playwright>=1.61.0",
|
|
40
|
+
"pytest-playwright>=0.8.0",
|
|
45
41
|
]
|
|
46
42
|
|
|
47
|
-
[tool.pdm.scripts]
|
|
48
|
-
test = "pytest"
|
|
49
|
-
lint = "ruff check ."
|
|
50
|
-
|
|
51
43
|
[tool.pytest.ini_options]
|
|
52
44
|
testpaths = [
|
|
53
45
|
"tests",
|
|
@@ -63,8 +55,19 @@ select = [
|
|
|
63
55
|
"W",
|
|
64
56
|
]
|
|
65
57
|
|
|
66
|
-
[
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
[tool.pdm]
|
|
59
|
+
distribution = true
|
|
60
|
+
|
|
61
|
+
[tool.pdm.version]
|
|
62
|
+
source = "scm"
|
|
63
|
+
|
|
64
|
+
[tool.pdm.build]
|
|
65
|
+
package-dir = "src"
|
|
66
|
+
includes = [
|
|
67
|
+
"src/pywebview_htmx",
|
|
70
68
|
]
|
|
69
|
+
|
|
70
|
+
[tool.pdm.scripts]
|
|
71
|
+
test = "pytest"
|
|
72
|
+
lint = "ruff check ."
|
|
73
|
+
install-browser = "playwright install chromium"
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
settleDelay: 20,
|
|
8
8
|
requestPolicy: "latest-wins",
|
|
9
9
|
};
|
|
10
|
-
const
|
|
10
|
+
const selfRequestState = new WeakMap();
|
|
11
|
+
const targetedRequestState = new Map();
|
|
11
12
|
const waitState = new WeakMap();
|
|
12
13
|
|
|
13
14
|
function $(selector, root = document) {
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
function triggerEvent(target, eventName, detail = {}) {
|
|
24
25
|
const event = new CustomEvent(eventName, {
|
|
25
26
|
bubbles: true,
|
|
26
|
-
cancelable:
|
|
27
|
+
cancelable: false,
|
|
27
28
|
detail,
|
|
28
29
|
});
|
|
29
30
|
target.dispatchEvent(event);
|
|
@@ -132,20 +133,21 @@
|
|
|
132
133
|
return await func(params);
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
function
|
|
136
|
-
|
|
136
|
+
function getRequestScope(element, targetSelector) {
|
|
137
|
+
const key = targetSelector || element;
|
|
138
|
+
const store = targetSelector ? targetedRequestState : selfRequestState;
|
|
139
|
+
let state = store.get(key);
|
|
137
140
|
if (!state) {
|
|
138
141
|
state = { lastIssued: 0, inFlightCount: 0 };
|
|
139
|
-
|
|
142
|
+
store.set(key, state);
|
|
140
143
|
}
|
|
141
|
-
return state;
|
|
144
|
+
return { key, state, store };
|
|
142
145
|
}
|
|
143
146
|
|
|
144
|
-
function
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
+
function releaseRequestScope(scope) {
|
|
148
|
+
if (scope.state.inFlightCount === 0 && scope.store.get(scope.key) === scope.state) {
|
|
149
|
+
scope.store.delete(scope.key);
|
|
147
150
|
}
|
|
148
|
-
return $(targetSelector) || element;
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
function addWaiting(waitTarget) {
|
|
@@ -164,8 +166,24 @@
|
|
|
164
166
|
waitState.set(waitTarget, count);
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
function shouldPreventDefault(eventName) {
|
|
168
|
-
|
|
169
|
+
function shouldPreventDefault(element, eventName) {
|
|
170
|
+
if (eventName === "submit") {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
if (eventName !== "click") {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if ((element.tagName === "A" || element.tagName === "AREA") && element.hasAttribute("href")) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
if (element.tagName === "BUTTON" && element.form) {
|
|
181
|
+
return element.type !== "button";
|
|
182
|
+
}
|
|
183
|
+
if (element.tagName === "INPUT" && element.form) {
|
|
184
|
+
return ["image", "reset", "submit"].includes(element.type);
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
169
187
|
}
|
|
170
188
|
|
|
171
189
|
function getRequestPolicy(element) {
|
|
@@ -195,9 +213,7 @@
|
|
|
195
213
|
|
|
196
214
|
const functionName = element.getAttribute("py-call");
|
|
197
215
|
const eventName = element.getAttribute("py-trigger") || "click";
|
|
198
|
-
const targetSelector = element.getAttribute("py-target");
|
|
199
|
-
const swapStyle = element.getAttribute("py-swap") || config.defaultSwapStyle;
|
|
200
|
-
const waitTarget = getWaitTarget(element);
|
|
216
|
+
const targetSelector = (element.getAttribute("py-target") || "").trim() || null;
|
|
201
217
|
|
|
202
218
|
if (!functionName) {
|
|
203
219
|
console.warn("PyWebview HTMX: py-call is empty", element);
|
|
@@ -205,12 +221,13 @@
|
|
|
205
221
|
}
|
|
206
222
|
|
|
207
223
|
element.addEventListener(eventName, async (event) => {
|
|
208
|
-
if (shouldPreventDefault(eventName) && event.cancelable) {
|
|
224
|
+
if (shouldPreventDefault(element, eventName) && event.cancelable) {
|
|
209
225
|
event.preventDefault();
|
|
210
226
|
}
|
|
211
227
|
|
|
212
228
|
const params = buildRequestParams(element, eventName, event);
|
|
213
|
-
const
|
|
229
|
+
const scope = getRequestScope(element, targetSelector);
|
|
230
|
+
const state = scope.state;
|
|
214
231
|
const requestPolicy = getRequestPolicy(element);
|
|
215
232
|
if (requestPolicy === "drop" && state.inFlightCount > 0) {
|
|
216
233
|
triggerEvent(element, "py:ignored", { reason: "in-flight" });
|
|
@@ -227,6 +244,8 @@
|
|
|
227
244
|
requestId,
|
|
228
245
|
});
|
|
229
246
|
|
|
247
|
+
const swapStyle = element.getAttribute("py-swap") || config.defaultSwapStyle;
|
|
248
|
+
const waitTarget = getWaitTarget(element);
|
|
230
249
|
addWaiting(waitTarget);
|
|
231
250
|
|
|
232
251
|
try {
|
|
@@ -265,9 +284,14 @@
|
|
|
265
284
|
}
|
|
266
285
|
} catch (error) {
|
|
267
286
|
console.error("PyWebview HTMX: error calling Python function", error);
|
|
268
|
-
triggerEvent(element, "py:error", {
|
|
287
|
+
triggerEvent(element, "py:error", {
|
|
288
|
+
error,
|
|
289
|
+
requestId,
|
|
290
|
+
stale: requestId !== state.lastIssued,
|
|
291
|
+
});
|
|
269
292
|
} finally {
|
|
270
293
|
state.inFlightCount = Math.max(0, state.inFlightCount - 1);
|
|
294
|
+
releaseRequestScope(scope);
|
|
271
295
|
removeWaiting(waitTarget);
|
|
272
296
|
}
|
|
273
297
|
});
|