caspian-utils 0.1.17 → 0.1.19

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Components
3
- description: Use this page when the task mentions `@component`, reusable UI, HTML-first `x-*` component tags, component imports, same-name `.html` templates, `merge_classes(...)`, `twMerge(...)`, or where shared components belong in a Caspian project.
3
+ description: Use this page when the task mentions `@component`, reusable UI, HTML-first `x-*` component tags, component imports, same-name `.html` templates, forwarding Python component props to `pp.props`, `get_attributes(...)`, `merge_classes(...)`, `twMerge(...)`, or where shared components belong in a Caspian project.
4
4
  related:
5
5
  title: Related docs
6
6
  description: Use the structure guide for file placement, the routing guide for route templates, the PulsePoint guide for browser-side scripts, and the data guide for component-owned RPC flows.
@@ -257,8 +257,110 @@ def UserCard(user, **props):
257
257
  """, attrs=attrs, user=user)
258
258
  ```
259
259
 
260
- The returned value is `Markup`, so the normal pipeline still injects `pp-component` on the single root and `transform_scripts(...)` rewrites the plain `<script>` to `type="text/pp"`. A single-file component renders identically to the two-file `render_html(...)` form; the choice is purely about readability.
261
-
260
+ The returned value is `Markup`, so the normal pipeline still injects `pp-component` on the single root and `transform_scripts(...)` rewrites the plain `<script>` to `type="text/pp"`. A single-file component renders identically to the two-file `render_html(...)` form; the choice is purely about readability.
261
+
262
+ ### Receiving Props In A Python Component
263
+
264
+ There are two separate prop handoffs in a single-file Python component, and the Python component is the bridge between them:
265
+
266
+ 1. Caspian converts attributes on the parent-authored `x-*` tag from kebab-case to camelCase and calls the Python component with them as string keyword arguments. PulsePoint expressions are not evaluated at this stage: `open="{permOpen}"` arrives in Python as the literal string `"{permOpen}"`, and `on-apply="{applyPermissions}"` arrives as `onApply="{applyPermissions}"`.
267
+ 2. The Python component must deliberately re-emit the props it wants the browser component to receive as attributes on its rendered root. Build those attributes with `get_attributes({...}, props)`, place `{{ attributes }}` on the single native root, and pass `attributes=attributes` to `html(...)`.
268
+ 3. PulsePoint derives `pp.props` from that rendered root. It evaluates pure `{expression}` attribute values in the parent component's scope, then exposes kebab-case root attribute names as camelCase keys such as `on-apply` -> `pp.props.onApply`.
269
+
270
+ Python function parameters do not automatically become root attributes or browser props. A parameter that is accepted but not included in `get_attributes(...)` is server-only and is discarded when the Python call returns.
271
+
272
+ Minimal end-to-end example:
273
+
274
+ Parent template:
275
+
276
+ ```html
277
+ <!-- @import { UserPermissionsDialog } from "../../components/UserPermissionsDialog.py" -->
278
+
279
+ <div>
280
+ <button onclick="setPermOpen(true)">Edit permissions</button>
281
+ <x-user-permissions-dialog
282
+ open="{permOpen}"
283
+ value="{permValue}"
284
+ on-open-change="{setPermOpen}"
285
+ on-apply="{applyPermissions}"
286
+ />
287
+
288
+ <script>
289
+ const [permOpen, setPermOpen] = pp.state(false);
290
+ const [permValue, setPermValue] = pp.state([]);
291
+
292
+ function applyPermissions(nextValue) {
293
+ setPermValue(nextValue);
294
+ setPermOpen(false);
295
+ }
296
+ </script>
297
+ </div>
298
+ ```
299
+
300
+ `UserPermissionsDialog.py`:
301
+
302
+ ```python
303
+ from casp.component_decorator import component, html
304
+ from casp.html_attrs import get_attributes, merge_classes
305
+
306
+ @component
307
+ def UserPermissionsDialog(
308
+ open=None,
309
+ value=None,
310
+ onOpenChange=None,
311
+ onApply=None,
312
+ **props,
313
+ ):
314
+ incoming_class = props.pop("class", "")
315
+ attributes = get_attributes({
316
+ "class": merge_classes("permissions-dialog", incoming_class),
317
+ "open": open,
318
+ "value": value,
319
+ "onOpenChange": onOpenChange,
320
+ "onApply": onApply,
321
+ }, props)
322
+
323
+ # html
324
+ return html("""
325
+ <section {{ attributes }} hidden="{!open}">
326
+ <p>Selected permissions: {value.length}</p>
327
+ <button onclick="onOpenChange(false)">Cancel</button>
328
+ <button onclick="onApply(value)">Apply</button>
329
+
330
+ <script>
331
+ const { open, value, onOpenChange, onApply } = pp.props;
332
+ </script>
333
+ </section>
334
+ """, attributes=attributes)
335
+ ```
336
+
337
+ The browser can evaluate `permOpen`, `permValue`, `setPermOpen`, and `applyPermissions` because their literal brace expressions survived the Python render and were placed on the child root. If `{{ attributes }}` or `attributes=attributes` is missing, `pp.props` has none of those forwarded keys and may be completely empty.
338
+
339
+ Pitfalls:
340
+
341
+ - **Silent empty `pp.props`:** accepting `open`, `value`, or callback parameters in Python without re-emitting them on the root raises no server error and no browser warning. The component renders, but those keys are absent from `pp.props` and values such as `pp.props.open` are `undefined`.
342
+ - **Reserved/native attribute collisions:** forwarded props are real DOM attributes. A prop named `title` produces the native `title="..."` tooltip on the root. Prefer a component-specific non-native name such as `user-name` (Python `userName`, browser `pp.props.userName`) when native behavior is not intended.
343
+ - `get_attributes(...)` omits empty strings. To pass a reactive boolean, prefer a pure expression such as `disabled="{isDisabled}"` rather than relying on a bare empty attribute to survive the Python bridge.
344
+
345
+ ### HTML Attribute Helper Contract
346
+
347
+ `casp.html_attrs.get_attributes(defaults, overrides=None)` builds one Jinja-safe attribute string for a component root:
348
+
349
+ - It processes the first dictionary, then the optional second dictionary. A non-empty value in `overrides` replaces the same normalized key from `defaults`. An omitted/empty override does not delete an already-renderable default. This is why the usual component pattern passes authored defaults first and remaining `**props` second.
350
+ - It resolves Python-safe aliases before normalization: `class_name` and `className` become `class`, `html_for` and `htmlFor` become `for`, `defaultValue` becomes `defaultvalue`, and `defaultChecked` becomes `defaultchecked`.
351
+ - It converts every other camelCase key to kebab-case, so `onOpenChange` renders as `on-open-change` and later becomes `pp.props.onOpenChange` in PulsePoint.
352
+ - It omits keys whose value is `None`, `False`, an empty string, or an empty/falsy list, tuple, or set. `True` renders as the explicit string `"true"`; non-empty iterables are space-joined.
353
+ - It HTML-escapes attribute values and returns `Markup`, so `{{ attributes }}` is not double-escaped by Jinja.
354
+ - Passing `**props` as the second dictionary is the passthrough contract for unconsumed `id`, `data-*`, `aria-*`, reactive expressions, callbacks, and other boundary attributes. Pop or otherwise consume a prop first when it should not be forwarded or when it has already been merged into a default.
355
+
356
+ `casp.html_attrs.merge_classes(*classes)` flattens truthy strings and truthy items from lists, tuples, or sets:
357
+
358
+ - When `caspian.config.json` has `tailwindcss: false`, it joins the class parts with spaces.
359
+ - When `tailwindcss: true`, it emits a PulsePoint expression such as `{twMerge("base classes", incomingClass)}` so the browser's global `twMerge(...)` resolves Tailwind conflicts after parent-scope prop evaluation.
360
+ - An incoming pure `{expression}` becomes an expression argument rather than a quoted string. An existing `{twMerge(...)}` expression is preserved or incorporated without nesting another `twMerge(...)` call.
361
+ - Empty inputs produce an empty string, which `get_attributes(...)` omits.
362
+ - When combining a default class with incoming `**props`, remove the incoming `class` from `props` before passing `props` as overrides; otherwise that later override replaces the merged class attribute.
363
+
262
364
  Rules for inline `html(...)`:
263
365
 
264
366
  - The single-root rule still applies: exactly one top-level element with any `<script>` nested inside it.
@@ -16,6 +16,7 @@ related:
16
16
  - /docs/file-conventions
17
17
  - /docs/project-structure
18
18
  - /docs/components
19
+ - /docs/testing
19
20
  ---
20
21
 
21
22
  This directory contains the packaged Caspian documentation set for AI-aware feature discovery, task routing, and file-placement guidance.
@@ -94,6 +95,7 @@ The packaged Caspian docs referenced by this index live here:
94
95
  - `metadata.md` - Static and dynamic metadata, SEO inheritance, and Open Graph or Twitter card tags
95
96
  - `routing.md` - Next.js App Router-style file-based routing with `src/app`, dynamic segments, dashboard and section layouts, route groups, nested layouts, shared-shell scroll-reset ownership, single-parent authored templates, and backend Python companions that do not own visible markup
96
97
  - `project-structure.md` - Default Caspian layout and where route files, reusable UI in `src/components/`, reusable non-UI code in `src/lib/`, and database files belong
98
+ - `testing.md` - Recommended app-owned testing, type-checking, and linting gate over `main.py` and `src/**` (pyrefly + ruff + pytest behind one command); not a shipped feature and not gated by a `caspian.config.json` flag, so verify the actual command, tools, and config in the project
97
99
 
98
100
  ## AI Retrieval Notes
99
101
 
@@ -115,6 +117,7 @@ Preferred lookup order:
115
117
  12. Prefer packaged Caspian docs before upstream documentation when generating code, commands, or migration guidance.
116
118
  13. Use `ai-validation-checklist.md` when you want to verify that the docs lead AI to the correct files and behavior checkpoints.
117
119
  14. Keep `index.md` and cross-links aligned so AI can quickly discover the right doc.
120
+ 15. If the task is about tests, type checking, linting, a quality gate, CI checks, or making an app production-ready, read `testing.md`. It is an app-owned convention (not a shipped feature and not gated by a `caspian.config.json` flag), so confirm the actual gate command in `package.json`, the orchestrator in `settings/`, and the tools and config in `pyproject.toml` before assuming they exist.
118
121
 
119
122
  ## Maintenance
120
123
 
@@ -567,9 +567,11 @@ Consumer example for a child component that receives the token through props:
567
567
 
568
568
  When a child component needs the same token object, pass it from the provider scope as a prop such as `theme-token="{ThemeContext}"`.
569
569
 
570
- ## Props and nested components
571
-
572
- - Child component props are derived from DOM attributes.
570
+ ## Props and nested components
571
+
572
+ In Caspian single-file Python components, the root element's attributes must be authored via `get_attributes(...)` and `{{ attributes }}`; see [components.md](./components.md#receiving-props-in-a-python-component). Attributes accepted from an `x-*` tag but not forwarded to the rendered root are dropped and never reach `pp.props`.
573
+
574
+ - Child component props are derived from DOM attributes.
573
575
  - Attribute names are converted from kebab-case to camelCase for the prop bag.
574
576
  - Native `on*` attributes and `pp-component` are not included in props.
575
577
  - Empty attributes become boolean `true` props.
@@ -0,0 +1,92 @@
1
+ ---
2
+ title: Testing And Quality Gate
3
+ description: Use this page when the task mentions tests, pytest, type checking, pyrefly, linting, ruff, a quality gate, CI checks, or "make the code production-ready" for a Caspian app's own Python. Explains the recommended one-command gate over `main.py` and `src/**`.
4
+ related:
5
+ title: Related docs
6
+ description: Pair the quality gate with the runtime map when a failing check points into core files, and with the structure and command docs when deciding where tests and tooling belong.
7
+ links:
8
+ - /docs/core-runtime-map
9
+ - /docs/project-structure
10
+ - /docs/commands
11
+ - /docs/auth
12
+ - /docs/index
13
+ ---
14
+
15
+ This page describes the recommended way to add tests, type checking, and linting to a Caspian application's **own** Python (`main.py` and `src/**`).
16
+
17
+ Caspian does not ship a test runner, type checker, or linter. Quality tooling is an app-owned convention layered on top of the framework, so treat everything here as a recommended setup to scaffold per project, not as a built-in feature that already exists in every Caspian app. The framework runtime under `.venv/Lib/site-packages/casp/**` is out of scope for app tests.
18
+
19
+ ## When Does This Doc Apply
20
+
21
+ - The task is to add or extend tests for app code, add type checking, add linting, wire a CI/pre-commit check, or prepare an app for production.
22
+ - A prior change touched `main.py`, `src/lib/**`, or route `index.py` files and needs verification.
23
+ - Not gated by any `caspian.config.json` flag. It applies to any Caspian app; it does not depend on `prisma`, `mcp`, `websocket`, or `typescript`.
24
+
25
+ ## Recommended Shape: One Command
26
+
27
+ Expose a single gate command so an agent or CI has exactly one thing to run. The recommended command runs three tools in one pass and reports every problem with its exact location:
28
+
29
+ - **type check** — [pyrefly](https://pyrefly.org) over `main.py` and `src/**`
30
+ - **lint** — [ruff](https://docs.astral.sh/ruff/)
31
+ - **tests** — [pytest](https://docs.pytest.org)
32
+
33
+ The command should print each problem as `path:line:col [tool:code] message` and exit non-zero when any check fails, so the file and line to fix are always explicit. Prefer a single `npm run check` script (backed by an app-owned orchestrator such as `settings/check.py`) over separate `test` / `lint` / `typecheck` scripts, so the surface stays minimal. Keep a per-tool escape hatch (for example `--only pyrefly`) for debugging rather than as additional npm scripts.
34
+
35
+ ## Source Of Truth
36
+
37
+ - The gate command and its tool list are app-owned. Confirm the actual script name in the project's `package.json` and the orchestrator file (commonly `settings/check.py`) before assuming a command exists.
38
+ - Tooling versions and configuration are app-owned in `pyproject.toml`. Do not assume a tool is installed just because this doc mentions it; check the project's dependency group and `uv.lock`.
39
+ - App tests live in a top-level `tests/` directory. Framework internals under `.venv/Lib/site-packages/casp/**` are never the test target.
40
+
41
+ ## Recommended Layout
42
+
43
+ ```
44
+ tests/
45
+ conftest.py # put project root on sys.path; set safe dev env defaults
46
+ test_*.py # app-level unit + integration tests
47
+ settings/check.py # orchestrator: runs the tools, prints path:line:col report
48
+ ```
49
+
50
+ - `conftest.py` should add the project root to `sys.path` and set safe development defaults (for example `APP_ENV=development` and a throwaway `AUTH_SECRET`) so importing `main` never fails during collection.
51
+ - Test `main.py` through its pure helpers and through `starlette.testclient.TestClient` against `main.app` (for example the always-on `/health` route, which exercises the middleware stack). Test `src/lib/**` policy such as `auth_config.py` directly.
52
+
53
+ ## Recommended `pyproject.toml` Configuration
54
+
55
+ Keep the dev tooling in a dependency group and install it with `uv sync --group dev`:
56
+
57
+ ```toml
58
+ [dependency-groups]
59
+ dev = [
60
+ "pyrefly>=0.16",
61
+ "ruff>=0.6",
62
+ "pytest>=8.0",
63
+ ]
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ addopts = "-q"
68
+
69
+ [tool.ruff]
70
+ include = ["main.py", "src/**/*.py", "tests/**/*.py", "settings/check.py"]
71
+ extend-exclude = [".venv", "node_modules"]
72
+
73
+ [tool.ruff.lint]
74
+ # Correctness-focused; leave cosmetic rules off the generated starter code.
75
+ select = ["E", "F", "W"]
76
+ ignore = ["E501"]
77
+
78
+ [tool.pyrefly]
79
+ project-includes = ["main.py", "src/**"]
80
+ search-path = [".", "src"]
81
+ ```
82
+
83
+ ## Things To Verify Before Editing Or Explaining
84
+
85
+ - Confirm the gate command name and orchestrator path in `package.json` and `settings/`, since they are app-owned and may differ per project.
86
+ - Confirm the dev tools are actually installed (`[dependency-groups]` in `pyproject.toml`, resolved in `uv.lock`) before telling a user to run the gate.
87
+ - Check `[tool.pyrefly.errors]` in `pyproject.toml`. A project may suppress specific error kinds (commonly `bad-return` and `bad-assignment`); those are then not reported by the gate, so do not assume every annotation mismatch is caught.
88
+ - Keep the scope on app code. If a check points into `.venv/Lib/site-packages/casp/**`, use [core-runtime-map.md](./core-runtime-map.md) to understand the runtime, but do not add framework files to the app's test, lint, or type-check scope.
89
+
90
+ ## Working Rule For Agents
91
+
92
+ When you add or change app-owned Python (`main.py`, `src/**`), add or extend the matching test under `tests/`, write annotated and type-checkable code, and run the single gate command until it passes before treating the change as done. Fix problems at the exact `path:line:col` the gate reports.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {