create-caspian-app 1.3.2 → 1.3.5
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.
|
@@ -256,6 +256,109 @@ def _iter_files() -> list[Path]:
|
|
|
256
256
|
return sorted(files)
|
|
257
257
|
|
|
258
258
|
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# f-string component returns (ratchet)
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# `html(...)` is the single markup entrypoint. A component that returns an
|
|
263
|
+
# f-string instead skips it entirely, and the two forms disagree in ways that
|
|
264
|
+
# are invisible at the call site:
|
|
265
|
+
#
|
|
266
|
+
# * The brace dialects are INVERTED. `html()` writes `{{ name }}` for server
|
|
267
|
+
# interpolation and `{count}` for a PulsePoint binding; an f-string writes
|
|
268
|
+
# `{name}` for the server and needs `{{count}}` to emit a binding. Same
|
|
269
|
+
# characters, opposite meanings.
|
|
270
|
+
# * There is no autoescaping. `Component.acall` wraps the returned string in
|
|
271
|
+
# `Markup`, so interpolated request data is emitted raw AND marked trusted.
|
|
272
|
+
# * `<x-*>` scope is not stashed, so a directly-called component
|
|
273
|
+
# (`{{ Card() }}`) cannot resolve nested component tags.
|
|
274
|
+
#
|
|
275
|
+
# The existing returns are recorded in a baseline and allowed; anything new
|
|
276
|
+
# fails the gate. Convert one and delete its baseline line. Regenerate with
|
|
277
|
+
# `python settings/check_templates.py --update-baseline`.
|
|
278
|
+
FSTRING_BASELINE_PATH = PROJECT_ROOT / "settings" / "fstring-components.json"
|
|
279
|
+
|
|
280
|
+
FSTRING_MESSAGE = (
|
|
281
|
+
"Component returns an f-string instead of html(...). The brace dialects are "
|
|
282
|
+
"inverted between the two forms ({{ x }} vs {x}) and an f-string is not "
|
|
283
|
+
"autoescaped, so interpolated data is emitted raw and marked trusted. "
|
|
284
|
+
"Return html(r'''...''', x=x) instead."
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _fstring_component_returns() -> list[tuple[str, str, int, int]]:
|
|
289
|
+
"""Every `@component` whose return value is an f-string.
|
|
290
|
+
|
|
291
|
+
Entries are `(rel_path, function_name, line, column)`, sorted.
|
|
292
|
+
"""
|
|
293
|
+
import ast
|
|
294
|
+
|
|
295
|
+
found: list[tuple[str, str, int, int]] = []
|
|
296
|
+
for path in _iter_files():
|
|
297
|
+
if path.suffix != ".py":
|
|
298
|
+
continue
|
|
299
|
+
try:
|
|
300
|
+
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
301
|
+
except (OSError, UnicodeDecodeError, SyntaxError):
|
|
302
|
+
continue
|
|
303
|
+
rel = path.relative_to(PROJECT_ROOT).as_posix()
|
|
304
|
+
for node in ast.walk(tree):
|
|
305
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
306
|
+
continue
|
|
307
|
+
decorators = {
|
|
308
|
+
getattr(d, "id", None) or getattr(d, "attr", None)
|
|
309
|
+
for d in node.decorator_list
|
|
310
|
+
}
|
|
311
|
+
if "component" not in decorators:
|
|
312
|
+
continue
|
|
313
|
+
for stmt in ast.walk(node):
|
|
314
|
+
if isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.JoinedStr):
|
|
315
|
+
found.append((rel, node.name, stmt.lineno, stmt.col_offset + 1))
|
|
316
|
+
break
|
|
317
|
+
return sorted(found)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _load_fstring_baseline() -> set[str]:
|
|
321
|
+
import json
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
raw = json.loads(FSTRING_BASELINE_PATH.read_text(encoding="utf-8"))
|
|
325
|
+
except (OSError, ValueError):
|
|
326
|
+
return set()
|
|
327
|
+
return set(raw.get("allowed", []))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def write_fstring_baseline() -> int:
|
|
331
|
+
"""Record today's f-string components as the allowed set."""
|
|
332
|
+
import json
|
|
333
|
+
|
|
334
|
+
entries = sorted({f"{rel}::{name}" for rel, name, _, _ in _fstring_component_returns()})
|
|
335
|
+
FSTRING_BASELINE_PATH.write_text(
|
|
336
|
+
json.dumps(
|
|
337
|
+
{
|
|
338
|
+
"_comment": (
|
|
339
|
+
"Components that still return an f-string instead of html(...). "
|
|
340
|
+
"This list may only shrink: converting one means deleting its "
|
|
341
|
+
"line. New entries fail `npm run check`."
|
|
342
|
+
),
|
|
343
|
+
"allowed": entries,
|
|
344
|
+
},
|
|
345
|
+
indent=2,
|
|
346
|
+
)
|
|
347
|
+
+ "\n",
|
|
348
|
+
encoding="utf-8",
|
|
349
|
+
)
|
|
350
|
+
return len(entries)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def lint_fstring_components() -> list[TemplateIssue]:
|
|
354
|
+
allowed = _load_fstring_baseline()
|
|
355
|
+
return [
|
|
356
|
+
TemplateIssue(rel, line, col, "fstring-component", FSTRING_MESSAGE)
|
|
357
|
+
for rel, name, line, col in _fstring_component_returns()
|
|
358
|
+
if f"{rel}::{name}" not in allowed
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
|
|
259
362
|
def lint_templates() -> list[TemplateIssue]:
|
|
260
363
|
"""Lint every authored template under `src/`."""
|
|
261
364
|
issues: list[TemplateIssue] = []
|
|
@@ -270,10 +373,21 @@ def lint_templates() -> list[TemplateIssue]:
|
|
|
270
373
|
continue
|
|
271
374
|
rel = path.relative_to(PROJECT_ROOT).as_posix()
|
|
272
375
|
issues.extend(lint_text(text, rel, is_python=path.suffix == ".py"))
|
|
376
|
+
issues.extend(lint_fstring_components())
|
|
273
377
|
return issues
|
|
274
378
|
|
|
275
379
|
|
|
276
380
|
def main() -> int:
|
|
381
|
+
import sys
|
|
382
|
+
|
|
383
|
+
if "--update-baseline" in sys.argv:
|
|
384
|
+
count = write_fstring_baseline()
|
|
385
|
+
print(
|
|
386
|
+
f"templates: recorded {count} f-string component(s) in "
|
|
387
|
+
f"{FSTRING_BASELINE_PATH.relative_to(PROJECT_ROOT).as_posix()}."
|
|
388
|
+
)
|
|
389
|
+
return 0
|
|
390
|
+
|
|
277
391
|
issues = lint_templates()
|
|
278
392
|
if not issues:
|
|
279
393
|
print("templates: no JSX or unknown directives found.")
|
package/dist/src/app/error.py
CHANGED
package/dist/src/app/layout.py
CHANGED