Skip to content

Commit 45a6796

Browse files
Iteration 144: attrs — user-defined metadata WeakMap registry
Added src/core/attrs.ts mirroring pandas' DataFrame.attrs / Series.attrs. Uses a WeakMap registry so immutable tsb objects can carry arbitrary key→value metadata without instance-property mutation. API: getAttrs, setAttrs, updateAttrs, withAttrs, copyAttrs, mergeAttrs, clearAttrs, hasAttrs, getAttr, setAttr, deleteAttr, attrsCount, attrsKeys. - src/core/attrs.ts — 13 exported functions + Attrs type alias - tests/core/attrs.test.ts — 40+ unit tests + 3 property-based tests (fast-check) - playground/attrs.html — interactive tutorial page with full API reference - src/core/index.ts — updated barrel exports - src/index.ts — re-exported from package root - playground/index.html — marked attrs as complete Run: https://github.com/githubnext/tsessebe/actions/runs/24196127191 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6515842 commit 45a6796

6 files changed

Lines changed: 1053 additions & 0 deletions

File tree

playground/attrs.html

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>tsb — attrs: user-defined metadata</title>
7+
<style>
8+
body { font-family: system-ui, sans-serif; max-width: 860px; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #1a1a1a; }
9+
h1 { color: #0066cc; }
10+
h2 { color: #333; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
11+
pre { background: #f6f8fa; border-radius: 6px; padding: 1rem; overflow-x: auto; font-size: 0.9em; }
12+
code { font-family: 'Fira Code', 'Cascadia Code', monospace; }
13+
.note { background: #fffbea; border-left: 4px solid #f5a623; padding: 0.7rem 1rem; border-radius: 0 6px 6px 0; margin: 1rem 0; }
14+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
15+
th, td { border: 1px solid #ddd; padding: 0.5rem 0.75rem; text-align: left; }
16+
th { background: #f0f4f8; }
17+
a { color: #0066cc; }
18+
.api-table td:first-child { font-family: monospace; white-space: nowrap; }
19+
</style>
20+
</head>
21+
<body>
22+
<p><a href="index.html">← tsb playground</a></p>
23+
24+
<h1><code>attrs</code> — User-Defined Metadata</h1>
25+
<p>
26+
Attach arbitrary key→value metadata to any <code>Series</code> or <code>DataFrame</code>
27+
— mirrors
28+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.attrs.html" target="_blank">
29+
<code>pandas.DataFrame.attrs</code></a> and
30+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.Series.attrs.html" target="_blank">
31+
<code>pandas.Series.attrs</code></a>.
32+
</p>
33+
34+
<div class="note">
35+
<strong>Design note:</strong> Because <code>tsb</code> objects are immutable (their data, index,
36+
and dtype are frozen), attrs are stored in a <strong>WeakMap registry</strong> rather than as
37+
instance properties. This means attrs are attached &amp; detached without touching the object
38+
itself, and garbage-collected automatically when the object is collected.
39+
</div>
40+
41+
<h2>Basic usage</h2>
42+
43+
<pre><code>import {
44+
getAttrs, setAttrs, updateAttrs, copyAttrs, withAttrs,
45+
clearAttrs, hasAttrs, getAttr, setAttr, deleteAttr,
46+
attrsCount, attrsKeys, mergeAttrs,
47+
} from "tsb";
48+
import { DataFrame, Series } from "tsb";
49+
50+
// ─── annotate a DataFrame ─────────────────────────────────────────────────
51+
const df = DataFrame.fromColumns({
52+
temperature: [22.1, 23.5, 21.8],
53+
humidity: [55, 60, 58],
54+
});
55+
56+
setAttrs(df, {
57+
source: "weather_station_42",
58+
unit: "Celsius",
59+
notes: "Morning readings",
60+
});
61+
62+
getAttrs(df);
63+
// → { source: "weather_station_42", unit: "Celsius", notes: "Morning readings" }
64+
65+
getAttr(df, "unit"); // → "Celsius"
66+
getAttr(df, "missing"); // → undefined
67+
attrsCount(df); // → 3
68+
attrsKeys(df); // → ["source", "unit", "notes"]
69+
hasAttrs(df); // → true
70+
</code></pre>
71+
72+
<h2>Merging and updating</h2>
73+
74+
<pre><code>// updateAttrs merges new keys, preserves existing
75+
updateAttrs(df, { version: 2, notes: "Updated notes" });
76+
getAttrs(df);
77+
// → { source: "weather_station_42", unit: "Celsius", notes: "Updated notes", version: 2 }
78+
79+
// setAttr / deleteAttr for single keys
80+
setAttr(df, "sensor_id", "WS-042");
81+
deleteAttr(df, "notes");
82+
getAttrs(df);
83+
// → { source: "weather_station_42", unit: "Celsius", version: 2, sensor_id: "WS-042" }
84+
</code></pre>
85+
86+
<h2>Propagating metadata to derived objects</h2>
87+
88+
<pre><code>// copyAttrs: copy all attrs from one object to another
89+
const s = new Series({ data: [22.1, 23.5, 21.8], name: "temperature" });
90+
setAttrs(s, { unit: "Celsius", source: "sensor_A" });
91+
92+
const derived = new Series({ data: [71.8, 74.3, 71.2], name: "fahrenheit" });
93+
copyAttrs(s, derived);
94+
getAttrs(derived);
95+
// → { unit: "Celsius", source: "sensor_A" }
96+
97+
// Then update the copy
98+
setAttr(derived, "unit", "Fahrenheit");
99+
getAttrs(derived); // → { unit: "Fahrenheit", source: "sensor_A" }
100+
getAttrs(s); // → { unit: "Celsius", source: "sensor_A" } ← unchanged
101+
</code></pre>
102+
103+
<h2>Fluent helper — <code>withAttrs</code></h2>
104+
105+
<pre><code>// withAttrs sets attrs and returns the same object reference
106+
// Handy for inline annotation
107+
const annotated = withAttrs(
108+
DataFrame.fromColumns({ x: [1, 2, 3] }),
109+
{ source: "lab_experiment", date: "2026-04-09" },
110+
);
111+
112+
annotated === annotated; // true — same reference, not a copy
113+
getAttrs(annotated);
114+
// → { source: "lab_experiment", date: "2026-04-09" }
115+
</code></pre>
116+
117+
<h2>Merging from multiple sources</h2>
118+
119+
<pre><code>// mergeAttrs: combine attrs from multiple objects into a target
120+
const s1 = new Series({ data: [1, 2, 3], name: "a" });
121+
const s2 = new Series({ data: [4, 5, 6], name: "b" });
122+
setAttrs(s1, { source: "sensor_A", unit: "kg" });
123+
setAttrs(s2, { source: "sensor_B", scale: 2.5 });
124+
125+
const combined = DataFrame.fromColumns({ a: [1, 2, 3], b: [4, 5, 6] });
126+
mergeAttrs([s1, s2], combined);
127+
// Later sources win on conflicts: source="sensor_B"
128+
getAttrs(combined);
129+
// → { source: "sensor_B", unit: "kg", scale: 2.5 }
130+
</code></pre>
131+
132+
<h2>Clearing metadata</h2>
133+
134+
<pre><code>setAttrs(df, { x: 1, y: 2 });
135+
hasAttrs(df); // → true
136+
attrsCount(df); // → 2
137+
138+
clearAttrs(df);
139+
hasAttrs(df); // → false
140+
getAttrs(df); // → {}
141+
</code></pre>
142+
143+
<h2>API reference</h2>
144+
145+
<table class="api-table">
146+
<thead>
147+
<tr><th>Function</th><th>Description</th></tr>
148+
</thead>
149+
<tbody>
150+
<tr><td>getAttrs(obj)</td><td>Return a shallow copy of all stored attrs (empty {} if none)</td></tr>
151+
<tr><td>setAttrs(obj, attrs)</td><td>Overwrite attrs completely with the given record</td></tr>
152+
<tr><td>updateAttrs(obj, updates)</td><td>Merge updates into existing attrs (existing keys preserved)</td></tr>
153+
<tr><td>withAttrs(obj, attrs)</td><td>Fluent: set attrs and return the same object</td></tr>
154+
<tr><td>copyAttrs(source, target)</td><td>Copy all attrs from source to target</td></tr>
155+
<tr><td>mergeAttrs(sources[], target)</td><td>Merge attrs from multiple sources; later sources win</td></tr>
156+
<tr><td>clearAttrs(obj)</td><td>Remove all attrs from obj</td></tr>
157+
<tr><td>hasAttrs(obj)</td><td>Return true if any attrs are set</td></tr>
158+
<tr><td>getAttr(obj, key)</td><td>Get a single attr value (undefined if missing)</td></tr>
159+
<tr><td>setAttr(obj, key, value)</td><td>Set a single attr, preserving other keys</td></tr>
160+
<tr><td>deleteAttr(obj, key)</td><td>Delete a single attr key</td></tr>
161+
<tr><td>attrsCount(obj)</td><td>Number of stored attr keys</td></tr>
162+
<tr><td>attrsKeys(obj)</td><td>Array of stored attr key names</td></tr>
163+
</tbody>
164+
</table>
165+
166+
<h2>Comparison with pandas</h2>
167+
168+
<table>
169+
<thead>
170+
<tr><th>pandas</th><th>tsb</th></tr>
171+
</thead>
172+
<tbody>
173+
<tr><td><code>df.attrs</code></td><td><code>getAttrs(df)</code></td></tr>
174+
<tr><td><code>df.attrs = {"k": "v"}</code></td><td><code>setAttrs(df, { k: "v" })</code></td></tr>
175+
<tr><td><code>df.attrs["k"] = "v"</code></td><td><code>setAttr(df, "k", "v")</code></td></tr>
176+
<tr><td><code>df.attrs["k"]</code></td><td><code>getAttr(df, "k")</code></td></tr>
177+
<tr><td><code>del df.attrs["k"]</code></td><td><code>deleteAttr(df, "k")</code></td></tr>
178+
<tr><td><code>df.attrs.update(d)</code></td><td><code>updateAttrs(df, d)</code></td></tr>
179+
<tr><td><code>df.attrs.clear()</code></td><td><code>clearAttrs(df)</code></td></tr>
180+
</tbody>
181+
</table>
182+
</body>
183+
</html>

playground/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,11 @@ <h3><a href="notna_isna.html" style="color: var(--accent); text-decoration: none
294294
<p>Module-level missing-value detection: <code>isna</code>, <code>notna</code>, <code>isnull</code>, <code>notnull</code> work on scalars, arrays, Series, and DataFrames. Plus standalone <code>fillna</code>, <code>dropna</code>, <code>countna</code>, and <code>countValid</code>. Mirrors <code>pandas.isna</code>, <code>pandas.notna</code>, <code>pandas.isnull</code>, <code>pandas.notnull</code>.</p>
295295
<div class="status done">✅ Complete</div>
296296
</div>
297+
<div class="feature-card">
298+
<h3><a href="attrs.html" style="color: var(--accent); text-decoration: none;">🏷️ attrs — User Metadata</a></h3>
299+
<p>Attach arbitrary key→value metadata to any <code>Series</code> or <code>DataFrame</code> via a <strong>WeakMap registry</strong>. Provides <code>getAttrs</code>, <code>setAttrs</code>, <code>updateAttrs</code>, <code>copyAttrs</code>, <code>withAttrs</code>, <code>mergeAttrs</code>, <code>clearAttrs</code>, <code>getAttr</code>, <code>setAttr</code>, <code>deleteAttr</code>, <code>attrsCount</code>, <code>attrsKeys</code>. Mirrors <code>pandas.DataFrame.attrs</code> / <code>pandas.Series.attrs</code>.</p>
300+
<div class="status done">✅ Complete</div>
301+
</div>
297302
</div>
298303
</section>
299304
</main>

0 commit comments

Comments
 (0)