pywebview-htmx 0.2.1__tar.gz → 0.3.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.2.1 → pywebview_htmx-0.3.0}/PKG-INFO +83 -3
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/README.md +82 -2
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/pyproject.toml +1 -1
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/src/pywebview_htmx/__init__.py +2 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/src/pywebview_htmx/runtime.py +10 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/src/pywebview_htmx/static/runtime.js +60 -3
- pywebview_htmx-0.3.0/tests/test_js_contract_ergonomics.py +125 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/tests/test_js_contract_request_and_swap.py +1 -1
- pywebview_htmx-0.3.0/tests/test_python_params_attr.py +19 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/LICENSE +0 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/src/pywebview_htmx/static/themes/aurora.css +0 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/src/pywebview_htmx/static/themes/base.css +0 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/src/pywebview_htmx/static/themes/cybermind.css +0 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/src/pywebview_htmx/static/themes/paper.css +0 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/tests/conftest.py +0 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/tests/test_js_contract_config_and_binding.py +0 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/tests/test_python_create_window.py +0 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/tests/test_python_injection.py +0 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/tests/test_python_script_loading.py +0 -0
- {pywebview_htmx-0.2.1 → pywebview_htmx-0.3.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.3.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
|
|
@@ -84,6 +84,34 @@ pdm install
|
|
|
84
84
|
pdm add pywebview-htmx
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
## Agent Skill
|
|
88
|
+
|
|
89
|
+
This repository includes an installable coding-agent skill at
|
|
90
|
+
`skills/use-pywebview-htmx/`.
|
|
91
|
+
|
|
92
|
+
The folder in this repository is the source for a skill that tools such as
|
|
93
|
+
[`npx skills`](https://skills.sh/) can discover and install from GitHub.
|
|
94
|
+
|
|
95
|
+
List the available skill from this repository:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx skills add btfranklin/pywebview-htmx --list --full-depth
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Install just this skill:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx skills add btfranklin/pywebview-htmx --skill use-pywebview-htmx
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Other skill installers can consume the same skill folder from this repository.
|
|
108
|
+
The skill includes:
|
|
109
|
+
|
|
110
|
+
- the core usage workflow
|
|
111
|
+
- a concise contract reference
|
|
112
|
+
- implementation patterns
|
|
113
|
+
- troubleshooting guidance
|
|
114
|
+
|
|
87
115
|
## Quick Start
|
|
88
116
|
|
|
89
117
|
```python
|
|
@@ -117,6 +145,18 @@ create_window("Quickstart", html, js_api=API())
|
|
|
117
145
|
You do **not** manually include `runtime.js` when using `create_window()`;
|
|
118
146
|
it is injected automatically.
|
|
119
147
|
|
|
148
|
+
## Important Differences From HTMX
|
|
149
|
+
|
|
150
|
+
- There is no HTTP request layer. `pywebview-htmx` calls a Python method on
|
|
151
|
+
`window.pywebview.api` directly.
|
|
152
|
+
- Python handlers should return an HTML string fragment. Returning objects or
|
|
153
|
+
other data types is treated as an error.
|
|
154
|
+
- For ordinary buttons/links, params still come from `data-py-params`.
|
|
155
|
+
- For `<form py-trigger="submit">`, named fields are serialized automatically
|
|
156
|
+
and merged over `data-py-params`.
|
|
157
|
+
- Returned HTML is inserted directly into the DOM, so escape untrusted values
|
|
158
|
+
before returning markup.
|
|
159
|
+
|
|
120
160
|
## API Reference (Python)
|
|
121
161
|
|
|
122
162
|
### `create_window(title, html, js_api=None, theme="aurora", start=True, **kwargs)`
|
|
@@ -143,6 +183,18 @@ Returns the bundled JavaScript runtime as a string.
|
|
|
143
183
|
|
|
144
184
|
Injects the runtime script tag into an HTML string (idempotent).
|
|
145
185
|
|
|
186
|
+
### `encode_params_attr(params)`
|
|
187
|
+
|
|
188
|
+
Returns HTML-escaped JSON that is safe to place in a `data-py-params`
|
|
189
|
+
attribute.
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from pywebview_htmx import encode_params_attr
|
|
193
|
+
|
|
194
|
+
payload = encode_params_attr({"user_id": 42, "mode": "full"})
|
|
195
|
+
button = f'<button data-py-params="{payload}">Load user</button>'
|
|
196
|
+
```
|
|
197
|
+
|
|
146
198
|
### Theme helpers
|
|
147
199
|
|
|
148
200
|
- `list_themes()` -> sorted list of available themes
|
|
@@ -183,6 +235,8 @@ JSON payload passed to Python method.
|
|
|
183
235
|
Notes:
|
|
184
236
|
- If missing, params default to `{}`.
|
|
185
237
|
- Invalid JSON logs an error and falls back to `{}`.
|
|
238
|
+
- For submit-triggered forms, named form fields are serialized automatically and
|
|
239
|
+
merged over this payload.
|
|
186
240
|
|
|
187
241
|
### `py-target` (optional)
|
|
188
242
|
|
|
@@ -203,6 +257,14 @@ Unknown value falls back to `innerHTML`.
|
|
|
203
257
|
CSS selector of element receiving `.py-waiting` while request is in flight.
|
|
204
258
|
If missing/empty/unresolvable, the triggering element is used.
|
|
205
259
|
|
|
260
|
+
### `py-policy` (optional)
|
|
261
|
+
|
|
262
|
+
Per-element concurrency override. If omitted, the global
|
|
263
|
+
`window.pywebviewHtmx.config.requestPolicy` value is used.
|
|
264
|
+
|
|
265
|
+
- `latest-wins`
|
|
266
|
+
- `drop`
|
|
267
|
+
|
|
206
268
|
## Runtime Config (JavaScript)
|
|
207
269
|
|
|
208
270
|
`window.pywebviewHtmx.config` includes:
|
|
@@ -297,7 +359,8 @@ behavior, add your own `<style data-pywebview-theme="my-theme">...</style>` bloc
|
|
|
297
359
|
### Pattern 1: Form submit to Python
|
|
298
360
|
|
|
299
361
|
- Use `py-trigger="submit"` on `<form>`
|
|
300
|
-
-
|
|
362
|
+
- Give fields `name` attributes so they can be serialized
|
|
363
|
+
- Use `data-py-params` only for extra static values you want merged in
|
|
301
364
|
- `py-target` a result card/summary region
|
|
302
365
|
|
|
303
366
|
### Pattern 2: Append activity/log rows
|
|
@@ -308,7 +371,8 @@ behavior, add your own `<style data-pywebview-theme="my-theme">...</style>` bloc
|
|
|
308
371
|
### Pattern 3: Replace full component
|
|
309
372
|
|
|
310
373
|
- Use `py-swap="outerHTML"`
|
|
311
|
-
- Return full replacement markup with same outer id/selector
|
|
374
|
+
- Return full replacement markup with the same outer id/selector if future
|
|
375
|
+
interactions should keep targeting that component
|
|
312
376
|
|
|
313
377
|
### Pattern 4: Theme switch from Python
|
|
314
378
|
|
|
@@ -331,6 +395,7 @@ Check:
|
|
|
331
395
|
- method returns a string of HTML
|
|
332
396
|
- no JSON parse error in `data-py-params`
|
|
333
397
|
- `py-target` selector exists
|
|
398
|
+
- for forms, fields have `name` attributes
|
|
334
399
|
|
|
335
400
|
### "New buttons in swapped HTML do not work"
|
|
336
401
|
|
|
@@ -345,6 +410,21 @@ window.pywebviewHtmx.process(document.body);
|
|
|
345
410
|
|
|
346
411
|
Style `.py-waiting` globally and/or point `py-wait` at a dedicated element.
|
|
347
412
|
|
|
413
|
+
### "I got a Python API return type error"
|
|
414
|
+
|
|
415
|
+
`pywebview-htmx` expects handlers to return an HTML string fragment. Convert
|
|
416
|
+
structured data into markup before returning it.
|
|
417
|
+
|
|
418
|
+
## Common Mistakes
|
|
419
|
+
|
|
420
|
+
- Returning dicts/lists instead of HTML strings
|
|
421
|
+
- Forgetting to escape untrusted values before interpolating into markup
|
|
422
|
+
- Omitting `name` attributes on form fields and expecting them to serialize
|
|
423
|
+
- Using `py-swap="outerHTML"` but not preserving the target component's
|
|
424
|
+
selector/id
|
|
425
|
+
- Manually mutating the DOM outside of `pywebview-htmx` swaps without calling
|
|
426
|
+
`window.pywebviewHtmx.process(...)`
|
|
427
|
+
|
|
348
428
|
## Demo
|
|
349
429
|
|
|
350
430
|
Run the full feature showcase:
|
|
@@ -44,6 +44,34 @@ pdm install
|
|
|
44
44
|
pdm add pywebview-htmx
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
## Agent Skill
|
|
48
|
+
|
|
49
|
+
This repository includes an installable coding-agent skill at
|
|
50
|
+
`skills/use-pywebview-htmx/`.
|
|
51
|
+
|
|
52
|
+
The folder in this repository is the source for a skill that tools such as
|
|
53
|
+
[`npx skills`](https://skills.sh/) can discover and install from GitHub.
|
|
54
|
+
|
|
55
|
+
List the available skill from this repository:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx skills add btfranklin/pywebview-htmx --list --full-depth
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Install just this skill:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx skills add btfranklin/pywebview-htmx --skill use-pywebview-htmx
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Other skill installers can consume the same skill folder from this repository.
|
|
68
|
+
The skill includes:
|
|
69
|
+
|
|
70
|
+
- the core usage workflow
|
|
71
|
+
- a concise contract reference
|
|
72
|
+
- implementation patterns
|
|
73
|
+
- troubleshooting guidance
|
|
74
|
+
|
|
47
75
|
## Quick Start
|
|
48
76
|
|
|
49
77
|
```python
|
|
@@ -77,6 +105,18 @@ create_window("Quickstart", html, js_api=API())
|
|
|
77
105
|
You do **not** manually include `runtime.js` when using `create_window()`;
|
|
78
106
|
it is injected automatically.
|
|
79
107
|
|
|
108
|
+
## Important Differences From HTMX
|
|
109
|
+
|
|
110
|
+
- There is no HTTP request layer. `pywebview-htmx` calls a Python method on
|
|
111
|
+
`window.pywebview.api` directly.
|
|
112
|
+
- Python handlers should return an HTML string fragment. Returning objects or
|
|
113
|
+
other data types is treated as an error.
|
|
114
|
+
- For ordinary buttons/links, params still come from `data-py-params`.
|
|
115
|
+
- For `<form py-trigger="submit">`, named fields are serialized automatically
|
|
116
|
+
and merged over `data-py-params`.
|
|
117
|
+
- Returned HTML is inserted directly into the DOM, so escape untrusted values
|
|
118
|
+
before returning markup.
|
|
119
|
+
|
|
80
120
|
## API Reference (Python)
|
|
81
121
|
|
|
82
122
|
### `create_window(title, html, js_api=None, theme="aurora", start=True, **kwargs)`
|
|
@@ -103,6 +143,18 @@ Returns the bundled JavaScript runtime as a string.
|
|
|
103
143
|
|
|
104
144
|
Injects the runtime script tag into an HTML string (idempotent).
|
|
105
145
|
|
|
146
|
+
### `encode_params_attr(params)`
|
|
147
|
+
|
|
148
|
+
Returns HTML-escaped JSON that is safe to place in a `data-py-params`
|
|
149
|
+
attribute.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from pywebview_htmx import encode_params_attr
|
|
153
|
+
|
|
154
|
+
payload = encode_params_attr({"user_id": 42, "mode": "full"})
|
|
155
|
+
button = f'<button data-py-params="{payload}">Load user</button>'
|
|
156
|
+
```
|
|
157
|
+
|
|
106
158
|
### Theme helpers
|
|
107
159
|
|
|
108
160
|
- `list_themes()` -> sorted list of available themes
|
|
@@ -143,6 +195,8 @@ JSON payload passed to Python method.
|
|
|
143
195
|
Notes:
|
|
144
196
|
- If missing, params default to `{}`.
|
|
145
197
|
- Invalid JSON logs an error and falls back to `{}`.
|
|
198
|
+
- For submit-triggered forms, named form fields are serialized automatically and
|
|
199
|
+
merged over this payload.
|
|
146
200
|
|
|
147
201
|
### `py-target` (optional)
|
|
148
202
|
|
|
@@ -163,6 +217,14 @@ Unknown value falls back to `innerHTML`.
|
|
|
163
217
|
CSS selector of element receiving `.py-waiting` while request is in flight.
|
|
164
218
|
If missing/empty/unresolvable, the triggering element is used.
|
|
165
219
|
|
|
220
|
+
### `py-policy` (optional)
|
|
221
|
+
|
|
222
|
+
Per-element concurrency override. If omitted, the global
|
|
223
|
+
`window.pywebviewHtmx.config.requestPolicy` value is used.
|
|
224
|
+
|
|
225
|
+
- `latest-wins`
|
|
226
|
+
- `drop`
|
|
227
|
+
|
|
166
228
|
## Runtime Config (JavaScript)
|
|
167
229
|
|
|
168
230
|
`window.pywebviewHtmx.config` includes:
|
|
@@ -257,7 +319,8 @@ behavior, add your own `<style data-pywebview-theme="my-theme">...</style>` bloc
|
|
|
257
319
|
### Pattern 1: Form submit to Python
|
|
258
320
|
|
|
259
321
|
- Use `py-trigger="submit"` on `<form>`
|
|
260
|
-
-
|
|
322
|
+
- Give fields `name` attributes so they can be serialized
|
|
323
|
+
- Use `data-py-params` only for extra static values you want merged in
|
|
261
324
|
- `py-target` a result card/summary region
|
|
262
325
|
|
|
263
326
|
### Pattern 2: Append activity/log rows
|
|
@@ -268,7 +331,8 @@ behavior, add your own `<style data-pywebview-theme="my-theme">...</style>` bloc
|
|
|
268
331
|
### Pattern 3: Replace full component
|
|
269
332
|
|
|
270
333
|
- Use `py-swap="outerHTML"`
|
|
271
|
-
- Return full replacement markup with same outer id/selector
|
|
334
|
+
- Return full replacement markup with the same outer id/selector if future
|
|
335
|
+
interactions should keep targeting that component
|
|
272
336
|
|
|
273
337
|
### Pattern 4: Theme switch from Python
|
|
274
338
|
|
|
@@ -291,6 +355,7 @@ Check:
|
|
|
291
355
|
- method returns a string of HTML
|
|
292
356
|
- no JSON parse error in `data-py-params`
|
|
293
357
|
- `py-target` selector exists
|
|
358
|
+
- for forms, fields have `name` attributes
|
|
294
359
|
|
|
295
360
|
### "New buttons in swapped HTML do not work"
|
|
296
361
|
|
|
@@ -305,6 +370,21 @@ window.pywebviewHtmx.process(document.body);
|
|
|
305
370
|
|
|
306
371
|
Style `.py-waiting` globally and/or point `py-wait` at a dedicated element.
|
|
307
372
|
|
|
373
|
+
### "I got a Python API return type error"
|
|
374
|
+
|
|
375
|
+
`pywebview-htmx` expects handlers to return an HTML string fragment. Convert
|
|
376
|
+
structured data into markup before returning it.
|
|
377
|
+
|
|
378
|
+
## Common Mistakes
|
|
379
|
+
|
|
380
|
+
- Returning dicts/lists instead of HTML strings
|
|
381
|
+
- Forgetting to escape untrusted values before interpolating into markup
|
|
382
|
+
- Omitting `name` attributes on form fields and expecting them to serialize
|
|
383
|
+
- Using `py-swap="outerHTML"` but not preserving the target component's
|
|
384
|
+
selector/id
|
|
385
|
+
- Manually mutating the DOM outside of `pywebview-htmx` swaps without calling
|
|
386
|
+
`window.pywebviewHtmx.process(...)`
|
|
387
|
+
|
|
308
388
|
## Demo
|
|
309
389
|
|
|
310
390
|
Run the full feature showcase:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .runtime import (
|
|
2
2
|
DEFAULT_THEME,
|
|
3
3
|
create_window,
|
|
4
|
+
encode_params_attr,
|
|
4
5
|
get_runtime_script,
|
|
5
6
|
get_theme_css,
|
|
6
7
|
inject_runtime,
|
|
@@ -11,6 +12,7 @@ from .runtime import (
|
|
|
11
12
|
__all__ = [
|
|
12
13
|
"DEFAULT_THEME",
|
|
13
14
|
"create_window",
|
|
15
|
+
"encode_params_attr",
|
|
14
16
|
"get_runtime_script",
|
|
15
17
|
"get_theme_css",
|
|
16
18
|
"inject_runtime",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from importlib.resources import files
|
|
4
|
+
import json
|
|
4
5
|
import re
|
|
6
|
+
from html import escape as html_escape
|
|
5
7
|
from typing import Any
|
|
6
8
|
|
|
7
9
|
import webview
|
|
@@ -56,6 +58,14 @@ def get_runtime_script() -> str:
|
|
|
56
58
|
)
|
|
57
59
|
|
|
58
60
|
|
|
61
|
+
def encode_params_attr(params: Any) -> str:
|
|
62
|
+
"""Encode a payload for safe embedding in ``data-py-params``."""
|
|
63
|
+
return html_escape(
|
|
64
|
+
json.dumps(params, sort_keys=True),
|
|
65
|
+
quote=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
59
69
|
def inject_runtime(html: str) -> str:
|
|
60
70
|
"""Inject the runtime script into an HTML string before ``</body>`` when present."""
|
|
61
71
|
if _SCRIPT_TAG_RE.search(html):
|
|
@@ -58,6 +58,47 @@
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function mergeParamValue(target, key, value) {
|
|
62
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
|
63
|
+
const existing = target[key];
|
|
64
|
+
if (Array.isArray(existing)) {
|
|
65
|
+
existing.push(value);
|
|
66
|
+
} else {
|
|
67
|
+
target[key] = [existing, value];
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
target[key] = value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function serializeForm(form, event) {
|
|
75
|
+
const params = {};
|
|
76
|
+
const formData = new FormData(form);
|
|
77
|
+
|
|
78
|
+
for (const [key, value] of formData.entries()) {
|
|
79
|
+
if (typeof File !== "undefined" && value instanceof File) {
|
|
80
|
+
console.warn("PyWebview HTMX: file inputs are not supported in data-py-params", key);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
mergeParamValue(params, key, value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const submitter = event && "submitter" in event ? event.submitter : null;
|
|
87
|
+
if (submitter && submitter.name) {
|
|
88
|
+
mergeParamValue(params, submitter.name, submitter.value || "");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return params;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildRequestParams(element, eventName, event) {
|
|
95
|
+
const params = parseParams(element);
|
|
96
|
+
if (element.tagName !== "FORM" || eventName !== "submit") {
|
|
97
|
+
return params;
|
|
98
|
+
}
|
|
99
|
+
return { ...params, ...serializeForm(element, event) };
|
|
100
|
+
}
|
|
101
|
+
|
|
61
102
|
function getWaitTarget(element) {
|
|
62
103
|
if (!element.hasAttribute("py-wait")) {
|
|
63
104
|
return element;
|
|
@@ -98,12 +139,24 @@
|
|
|
98
139
|
return eventName === "submit";
|
|
99
140
|
}
|
|
100
141
|
|
|
142
|
+
function getRequestPolicy(element) {
|
|
143
|
+
const policy = element.getAttribute("py-policy") || config.requestPolicy;
|
|
144
|
+
if (policy === "drop" || policy === "latest-wins") {
|
|
145
|
+
return policy;
|
|
146
|
+
}
|
|
147
|
+
return config.requestPolicy;
|
|
148
|
+
}
|
|
149
|
+
|
|
101
150
|
function processPyWebviewHtmxNodes(root = document) {
|
|
102
151
|
if (!root || typeof root.querySelectorAll !== "function") {
|
|
103
152
|
return;
|
|
104
153
|
}
|
|
105
154
|
|
|
106
|
-
const elements =
|
|
155
|
+
const elements = [];
|
|
156
|
+
if (root.matches && root.matches("[py-call]")) {
|
|
157
|
+
elements.push(root);
|
|
158
|
+
}
|
|
159
|
+
elements.push(...root.querySelectorAll("[py-call]"));
|
|
107
160
|
|
|
108
161
|
elements.forEach((element) => {
|
|
109
162
|
if (element.getAttribute("data-pywebview-bound") === "true") {
|
|
@@ -127,9 +180,10 @@
|
|
|
127
180
|
event.preventDefault();
|
|
128
181
|
}
|
|
129
182
|
|
|
130
|
-
const params =
|
|
183
|
+
const params = buildRequestParams(element, eventName, event);
|
|
131
184
|
const state = getRequestState(element);
|
|
132
|
-
|
|
185
|
+
const requestPolicy = getRequestPolicy(element);
|
|
186
|
+
if (requestPolicy === "drop" && state.inFlightCount > 0) {
|
|
133
187
|
triggerEvent(element, "py:ignored", { reason: "in-flight" });
|
|
134
188
|
return;
|
|
135
189
|
}
|
|
@@ -151,6 +205,9 @@
|
|
|
151
205
|
if (requestId !== state.lastIssued) {
|
|
152
206
|
return;
|
|
153
207
|
}
|
|
208
|
+
if (typeof response !== "string") {
|
|
209
|
+
throw new Error("PyWebview HTMX: Python API methods must return an HTML string");
|
|
210
|
+
}
|
|
154
211
|
|
|
155
212
|
const target = targetSelector ? $(targetSelector) : element;
|
|
156
213
|
if (!target) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
TESTED_BEHAVIOR_IDS = set(range(101, 114))
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _assert_fragments_present(js_compact: str, fragments: tuple[str, ...]) -> None:
|
|
9
|
+
for fragment in fragments:
|
|
10
|
+
assert fragment in js_compact
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
JS_BEHAVIOR_FRAGMENTS: list[tuple[int, tuple[str, ...]]] = [
|
|
14
|
+
(
|
|
15
|
+
101,
|
|
16
|
+
(
|
|
17
|
+
"function mergeParamValue(target, key, value)",
|
|
18
|
+
"if (Object.prototype.hasOwnProperty.call(target, key))",
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
(
|
|
22
|
+
102,
|
|
23
|
+
(
|
|
24
|
+
"function serializeForm(form, event)",
|
|
25
|
+
"const formData = new FormData(form);",
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
(
|
|
29
|
+
103,
|
|
30
|
+
(
|
|
31
|
+
'const submitter = event && "submitter" in event ? event.submitter : null;',
|
|
32
|
+
"submitter.name",
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
(
|
|
36
|
+
104,
|
|
37
|
+
(
|
|
38
|
+
"value instanceof File",
|
|
39
|
+
(
|
|
40
|
+
'console.warn("PyWebview HTMX: file inputs are not supported '
|
|
41
|
+
'in data-py-params"'
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
(
|
|
46
|
+
105,
|
|
47
|
+
(
|
|
48
|
+
"function buildRequestParams(element, eventName, event)",
|
|
49
|
+
'if (element.tagName !== "FORM" || eventName !== "submit")',
|
|
50
|
+
"return { ...params, ...serializeForm(element, event) };",
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
(
|
|
54
|
+
106,
|
|
55
|
+
(
|
|
56
|
+
"const elements = [];",
|
|
57
|
+
'if (root.matches && root.matches("[py-call]"))',
|
|
58
|
+
"elements.push(root);",
|
|
59
|
+
'elements.push(...root.querySelectorAll("[py-call]"));',
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
(
|
|
63
|
+
107,
|
|
64
|
+
(
|
|
65
|
+
"function getRequestPolicy(element)",
|
|
66
|
+
'const policy = element.getAttribute("py-policy") || config.requestPolicy;',
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
(
|
|
70
|
+
108,
|
|
71
|
+
(
|
|
72
|
+
'policy === "drop" || policy === "latest-wins"',
|
|
73
|
+
"return config.requestPolicy;",
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
109,
|
|
78
|
+
(
|
|
79
|
+
"const requestPolicy = getRequestPolicy(element);",
|
|
80
|
+
'if (requestPolicy === "drop" && state.inFlightCount > 0)',
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
(
|
|
84
|
+
110,
|
|
85
|
+
(
|
|
86
|
+
"const params = buildRequestParams(element, eventName, event);",
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
(
|
|
90
|
+
111,
|
|
91
|
+
(
|
|
92
|
+
'if (typeof response !== "string") {',
|
|
93
|
+
(
|
|
94
|
+
'throw new Error("PyWebview HTMX: Python API methods must return '
|
|
95
|
+
'an HTML string")'
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
(
|
|
100
|
+
112,
|
|
101
|
+
(
|
|
102
|
+
'triggerEvent(element, "py:error", { error, requestId });',
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
(
|
|
106
|
+
113,
|
|
107
|
+
(
|
|
108
|
+
'element.getAttribute("py-policy")',
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@pytest.mark.parametrize(
|
|
115
|
+
("behavior_id", "fragments"),
|
|
116
|
+
JS_BEHAVIOR_FRAGMENTS,
|
|
117
|
+
ids=[f"B{behavior_id}" for behavior_id, _ in JS_BEHAVIOR_FRAGMENTS],
|
|
118
|
+
)
|
|
119
|
+
def test_js_ergonomic_contract_fragments(
|
|
120
|
+
behavior_id: int,
|
|
121
|
+
fragments: tuple[str, ...],
|
|
122
|
+
js_compact: str,
|
|
123
|
+
) -> None:
|
|
124
|
+
_ = behavior_id
|
|
125
|
+
_assert_fragments_present(js_compact, fragments)
|
|
@@ -23,7 +23,7 @@ JS_BEHAVIOR_FRAGMENTS: list[tuple[int, tuple[str, ...]]] = [
|
|
|
23
23
|
(74, ("let state = requestState.get(element);", "if (!state) {")),
|
|
24
24
|
(75, ("lastIssued: 0", "state.lastIssued = requestId;")),
|
|
25
25
|
(76, ("inFlightCount: 0", "state.inFlightCount += 1;")),
|
|
26
|
-
(77, ('
|
|
26
|
+
(77, ('requestPolicy === "drop"', 'triggerEvent(element, "py:ignored"')),
|
|
27
27
|
(78, ('triggerEvent(element, "py:trigger"', "requestId,")),
|
|
28
28
|
(79, ('waitTarget.classList.add("py-waiting")',)),
|
|
29
29
|
(80, ("if (requestId !== state.lastIssued) { return; }",)),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pywebview_htmx import encode_params_attr
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_encode_params_attr_returns_json_string() -> None:
|
|
7
|
+
encoded = encode_params_attr({"count": 3, "enabled": True})
|
|
8
|
+
assert encoded == "{"count": 3, "enabled": true}"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_encode_params_attr_escapes_quotes_and_tags() -> None:
|
|
12
|
+
encoded = encode_params_attr({"message": '"hi" <b>unsafe</b>'})
|
|
13
|
+
assert "\\"hi\\"" in encoded
|
|
14
|
+
assert "<b>unsafe</b>" in encoded
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_encode_params_attr_is_exported_from_package() -> None:
|
|
18
|
+
encoded = encode_params_attr({"name": "pywebview-htmx"})
|
|
19
|
+
assert isinstance(encoded, str)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pywebview_htmx-0.2.1 → pywebview_htmx-0.3.0}/src/pywebview_htmx/static/themes/cybermind.css
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|