unstd 1.0.5__tar.gz → 2.0.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.
- unstd-2.0.0/CHANGELOG.md +385 -0
- {unstd-1.0.5 → unstd-2.0.0}/PKG-INFO +6 -8
- {unstd-1.0.5 → unstd-2.0.0}/README.md +4 -4
- {unstd-1.0.5 → unstd-2.0.0}/bench/README.md +11 -2
- {unstd-1.0.5 → unstd-2.0.0}/bench/baseline.json +20 -10
- {unstd-1.0.5 → unstd-2.0.0}/bench/cases.py +87 -16
- {unstd-1.0.5 → unstd-2.0.0}/bench/report.py +10 -2
- {unstd-1.0.5 → unstd-2.0.0}/pyproject.toml +6 -6
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/__init__.py +12 -9
- unstd-2.0.0/src/unstd/_extra.py +28 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/audio/README.md +11 -11
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/audio/wavx.py +85 -53
- unstd-2.0.0/src/unstd/clone/README.md +91 -0
- unstd-2.0.0/src/unstd/clone/__init__.py +27 -0
- unstd-2.0.0/src/unstd/clone/structural.py +277 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/containers/persistent.py +4 -4
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/containers/sorted.py +6 -5
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/crypto/README.md +17 -14
- unstd-2.0.0/src/unstd/crypto/__init__.py +20 -0
- unstd-2.0.0/src/unstd/crypto/_blake3.py +19 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/crypto/digest.py +53 -80
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/crypto/tls.py +11 -6
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/crypto/token.py +46 -51
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/fs/README.md +5 -3
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/fs/__init__.py +44 -47
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/ids/README.md +6 -9
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/ids/__init__.py +5 -5
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/ids/uid.py +43 -41
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/iters/README.md +6 -6
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/iters/__init__.py +2 -2
- unstd-1.0.5/src/unstd/iters/runs.py → unstd-2.0.0/src/unstd/iters/consecutive.py +17 -8
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/iters/distinct.py +24 -10
- unstd-2.0.0/src/unstd/iters/reshape.py +52 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/iters/select.py +10 -2
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/pack/binpack.py +5 -4
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/proc/README.md +4 -3
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/proc/__init__.py +6 -8
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rand/__init__.py +6 -6
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rand/crypto.py +12 -28
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rand/draw.py +36 -57
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rex/README.md +17 -12
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rex/__init__.py +101 -80
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/serde/README.md +14 -11
- unstd-2.0.0/src/unstd/serde/b64.py +127 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/serde/jsonx.py +61 -44
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/serde/ndjson.py +50 -22
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/serde/structs.py +19 -40
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/text/fuzz.py +141 -117
- unstd-2.0.0/src/unstd/time/README.md +69 -0
- unstd-2.0.0/src/unstd/time/__init__.py +15 -0
- unstd-2.0.0/src/unstd/time/timeutil.py +292 -0
- unstd-2.0.0/src/unstd/time/zoned.py +133 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/toml/README.md +1 -1
- unstd-2.0.0/src/unstd/toml/__init__.py +129 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/audio/test_audio.py +74 -49
- unstd-2.0.0/tests/clone/test_clone.py +386 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/conftest.py +3 -3
- {unstd-1.0.5 → unstd-2.0.0}/tests/containers/test_containers.py +3 -3
- {unstd-1.0.5 → unstd-2.0.0}/tests/crypto/test_adverse.py +51 -41
- {unstd-1.0.5 → unstd-2.0.0}/tests/crypto/test_vectors.py +15 -15
- {unstd-1.0.5 → unstd-2.0.0}/tests/fs/test_fs.py +28 -26
- {unstd-1.0.5 → unstd-2.0.0}/tests/iters/test_iters.py +79 -12
- {unstd-1.0.5 → unstd-2.0.0}/tests/proc/test_proc.py +8 -5
- {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_clone_invariants.py +21 -14
- {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_toml_invariants.py +13 -6
- {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_wav_quant.py +2 -2
- {unstd-1.0.5 → unstd-2.0.0}/tests/rex/test_rex.py +102 -0
- unstd-2.0.0/tests/serde/test_b64.py +135 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/serde/test_ndjson.py +56 -18
- {unstd-1.0.5 → unstd-2.0.0}/tests/serde/test_structs.py +19 -19
- {unstd-1.0.5 → unstd-2.0.0}/tests/test_bench.py +16 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/test_package.py +1 -1
- {unstd-1.0.5 → unstd-2.0.0}/tests/text/test_text.py +42 -1
- {unstd-1.0.5 → unstd-2.0.0}/tests/time/test_timeutil.py +170 -47
- unstd-2.0.0/tests/time/test_zoned.py +172 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/toml/test_toml.py +36 -18
- {unstd-1.0.5 → unstd-2.0.0}/uv.lock +10 -16
- unstd-1.0.5/CHANGELOG.md +0 -184
- unstd-1.0.5/src/unstd/clone/README.md +0 -90
- unstd-1.0.5/src/unstd/clone/__init__.py +0 -32
- unstd-1.0.5/src/unstd/clone/structural.py +0 -126
- unstd-1.0.5/src/unstd/containers/guard.py +0 -48
- unstd-1.0.5/src/unstd/crypto/__init__.py +0 -34
- unstd-1.0.5/src/unstd/ids/hash.py +0 -39
- unstd-1.0.5/src/unstd/iters/reshape.py +0 -56
- unstd-1.0.5/src/unstd/serde/b64.py +0 -142
- unstd-1.0.5/src/unstd/time/README.md +0 -47
- unstd-1.0.5/src/unstd/time/__init__.py +0 -14
- unstd-1.0.5/src/unstd/time/dateutil.py +0 -194
- unstd-1.0.5/src/unstd/time/timeutil.py +0 -219
- unstd-1.0.5/src/unstd/toml/__init__.py +0 -201
- unstd-1.0.5/tests/clone/test_clone.py +0 -230
- unstd-1.0.5/tests/serde/test_b64.py +0 -117
- unstd-1.0.5/tests/time/test_dateutil.py +0 -305
- {unstd-1.0.5 → unstd-2.0.0}/.github/workflows/ci.yml +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/.github/workflows/release.yml +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/.gitignore +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/LICENSE +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/NOTICE +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/bench/__init__.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/bench/__main__.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/bench/timing.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/audio/__init__.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/containers/README.md +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/containers/__init__.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/model/README.md +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/model/__init__.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/model/base.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/model/settings.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/pack/README.md +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/pack/__init__.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/py.typed +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rand/README.md +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/serde/__init__.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/text/README.md +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/src/unstd/text/__init__.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/crypto/blake3_reference_vectors.json +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/ids/test_uid.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/model/test_model.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/pack/test_pack.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/properties/README.md +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_pack_invariants.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_serde_parity.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_uuid7.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/rand/test_rand.py +0 -0
- {unstd-1.0.5 → unstd-2.0.0}/tests/serde/test_jsonx.py +0 -0
unstd-2.0.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
<!-- towncrier release notes start -->
|
|
4
|
+
|
|
5
|
+
## [2.0.0] - 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Breaking
|
|
8
|
+
|
|
9
|
+
This is a 2.0 surface pass: every change below renames or removes a public name,
|
|
10
|
+
and each old name fails at import rather than changing meaning in place.
|
|
11
|
+
|
|
12
|
+
- `serde.b64` is one verb per direction. `b64s` / `b64u_s` → `encode(data, url=,
|
|
13
|
+
pad=)`, `b64d` / `b64u_d` → `decode(s, url=)`, `b64text` / `b64u_text` →
|
|
14
|
+
`decode_text`, `b64_json` / `unb64_json` → `encode_json` / `decode_json`.
|
|
15
|
+
`pad=False` is new - the unpadded base64url JWTs and bearer tokens use, which
|
|
16
|
+
callers were `rstrip("=")`-ing by hand - and url-safe `decode` now accepts
|
|
17
|
+
padded and unpadded input alike. Standard `decode` stays strict.
|
|
18
|
+
- `ids.hash` is gone; it was three second names for `crypto.digest`. Content
|
|
19
|
+
hashes are `digest.hex(data)`, short ids `digest.hex(data, n)`, raw bytes
|
|
20
|
+
`digest.raw(data)` (was `sum_` / `hex_` / `hex_n` / `content_hash`).
|
|
21
|
+
`digest.of_parts(parts)` is `digest.Stream(parts).raw()`; `Stream.update_all`
|
|
22
|
+
went with it, and `Stream.sum_` / `hex_` are `raw` / `hex(n=None)`.
|
|
23
|
+
`digest.KDF_BYTES` folded into `DIGEST_BYTES` (both were 32).
|
|
24
|
+
- `crypto.token.mac` / `mac_bytes` → `mac_hex` / `mac_raw`, so the return type is
|
|
25
|
+
in the name as it is in `digest`. `verify_mac` takes a tag in either form.
|
|
26
|
+
`equal` lives in `token` only, and `token.opaque` is
|
|
27
|
+
`rand.crypto.token_urlsafe` - it was a pass-through.
|
|
28
|
+
- `time.dateutil` is `time.zoned`, holding only what needs `whenever`:
|
|
29
|
+
`wall` / `parse` / `now` / `instant` / `to_utc` (were `zoned_wall` /
|
|
30
|
+
`parse_zoned` / `zoned_now` / `parse_instant` / `zoned_to_utc`). The pure-stdlib
|
|
31
|
+
half - `parse_iso`, `parse_utc`, `parse_date`, `from_epoch_s`, `from_epoch_ms` -
|
|
32
|
+
moved to `timeutil`, so none of it needs the `time` extra any more.
|
|
33
|
+
- `timeutil.span(start, end=None, *, unit, assume_utc=False)` replaces
|
|
34
|
+
`span_hours`, `span_minutes`, `age_days`, `minutes_until`, `minutes_since`, and
|
|
35
|
+
`hours_between`. Those five disagreed about whether a floating stamp was UTC
|
|
36
|
+
depending on which unit you asked in; `span` is strict unless told otherwise.
|
|
37
|
+
`iso_range`, `lookback`, `days_range`, and `day_offset` are removed.
|
|
38
|
+
- `timeutil.iso(dt=None, *, ms=False)` replaces `utcnow_iso`, `utcnow_iso_ms`,
|
|
39
|
+
and `iso_fmt`; `today(utc=True)` replaces `utcnow_date_iso`; `today_iso` and
|
|
40
|
+
`date_fmt` are removed (`.isoformat()`); `human(tz="")` replaces `human_now`
|
|
41
|
+
and `human_now_tz`.
|
|
42
|
+
- `timeutil.parse_date` returns `None` on bad input like every other parser
|
|
43
|
+
here, instead of raising.
|
|
44
|
+
- `serde.structs.codec(...)` is removed; construct `structs.Codec(...)`.
|
|
45
|
+
- `audio.wavx` drops the stutter: `read_wav` / `write_wav` / `read_blocks` /
|
|
46
|
+
`WavWriter` → `read` / `write` / `blocks` / `Writer`.
|
|
47
|
+
- `fs.read_bytes`, `fs.read_text`, and `fs.iter_dir` are removed. They measured
|
|
48
|
+
slower than the `pathlib` methods they wrapped.
|
|
49
|
+
- `proc.TimeoutError` is removed. It aliased `subprocess.TimeoutExpired`, which
|
|
50
|
+
does not subclass the builtin of that name.
|
|
51
|
+
- `iters.take(iterable, n)` takes the iterable first, like `chunked`, `windowed`,
|
|
52
|
+
and `nth` beside it. The old order fails loudly (`'int' object is not
|
|
53
|
+
iterable`), never silently.
|
|
54
|
+
- `iters.batched_with_key` is `iters.runs` - it groups consecutive runs by key;
|
|
55
|
+
it never batched. Its module is `iters.consecutive`.
|
|
56
|
+
- `iters.partition(iterable, pred)` takes the iterable first, like every other
|
|
57
|
+
combinator in `iters`. The old order raises `TypeError` at the call.
|
|
58
|
+
- `serde.ndjson.read(fp)` / `write(fp, rows)` → `load(fp)` / `dump(rows, fp)`,
|
|
59
|
+
the same shape as `jsonx.dump(obj, fp)` and `toml.dump(obj, fp)`. `dump` still
|
|
60
|
+
returns the record count.
|
|
61
|
+
- `timeutil.now_tz(tz)` requires its zone. Called bare it was a second spelling
|
|
62
|
+
of `utcnow()`.
|
|
63
|
+
- `toml`'s write half is `tomlkit`'s own functions rather than wrappers around
|
|
64
|
+
them, so `dumps` / `dump` accept `sort_keys=` and each name carries tomlkit's
|
|
65
|
+
types. On a base install each is a placeholder that raises naming the extra,
|
|
66
|
+
decided at import rather than re-checked per call.
|
|
67
|
+
- `unstd.crypto` no longer re-exports the `blake3` class; it was the backend,
|
|
68
|
+
not a surface. The guard moved into `digest` / `token`, so `crypto.tls` - pure
|
|
69
|
+
stdlib - now imports on a base install instead of failing on a missing BLAKE3.
|
|
70
|
+
- `crypto.tls.client_context(certfile=...)` alone loads the chain, which is the
|
|
71
|
+
combined cert-plus-key PEM most bundles ship as; it used to be ignored and the
|
|
72
|
+
client dialed without mTLS. A `keyfile` with no `certfile` raises `ValueError`.
|
|
73
|
+
- The `ids` extra no longer pulls `blake3`; nothing under `ids` has used it since
|
|
74
|
+
`ids.hash` folded into `crypto.digest`. Depend on `unstd[crypto]` for digests.
|
|
75
|
+
- `unstd.containers.guard` is gone. Its placeholder and `toml`'s copy of it are
|
|
76
|
+
one private helper, and every missing-backend name now raises the same
|
|
77
|
+
`unstd.<module>.<name> requires the '<extra>' extra` message.
|
|
78
|
+
|
|
79
|
+
### Performance
|
|
80
|
+
|
|
81
|
+
- `jsonx.dumps` 416 → 264 ns, `dumpb` 375 → 200 ns, `loads` 251 → 212 ns on a
|
|
82
|
+
small dict (raw orjson: ~180 ns). The fast path no longer detours through a
|
|
83
|
+
checking wrapper, and `loads` names its return type with an annotation instead
|
|
84
|
+
of a runtime `typing.cast` call.
|
|
85
|
+
- `ndjson.dumpb` / binary `write` let orjson append each newline
|
|
86
|
+
(`OPT_APPEND_NEWLINE`) instead of concatenating a second `bytes` per row.
|
|
87
|
+
- `rand`'s scalar draws are stdlib `random` on every install. Routing
|
|
88
|
+
`random()` / `choice` / `shuffle` through numpy's `Generator` one value at a
|
|
89
|
+
time was 5-7x slower than the stdlib draw; numpy still fills the bulk arrays.
|
|
90
|
+
- `timeutil.wall` / `mono` / `mono_ns` / `perf` and `rand.crypto.*` are the stdlib
|
|
91
|
+
functions bound directly, with no forwarding frame.
|
|
92
|
+
- `timeutil.iso` formats through `isoformat` (~2.6x faster than `strftime`), and
|
|
93
|
+
`epoch_ms` is exact integer math on `time_ns()`.
|
|
94
|
+
- `structs.Codec.encode` / `decode` are msgspec's own bound methods.
|
|
95
|
+
- `digest.hex(data, n)` asks the XOF for `n` bytes rather than slicing a full
|
|
96
|
+
hex string, and checks the width inline.
|
|
97
|
+
- `jsonx.canonical` reuses one prebuilt encoder instead of letting `json.dumps`
|
|
98
|
+
construct a fresh one per call: ~1.25 µs → ~0.9 µs on a small dict, same bytes.
|
|
99
|
+
- `b64.encode(url=True)` rides pybase64's `altchars` straight to `str`: 293 →
|
|
100
|
+
192 ns for 64 bytes unpadded.
|
|
101
|
+
- `token.mac_hex` / `mac_raw` check the key inline - 943 → 674 ns - and `verify`
|
|
102
|
+
follows (2.1 → 1.6 µs).
|
|
103
|
+
- `ndjson.loads` parses a clean blob in one comprehension over the backend
|
|
104
|
+
decoder and strips a line only when a direct parse fails: 338 → 187 µs per
|
|
105
|
+
1k records, level with a bare orjson loop. A bad line still re-walks the slow
|
|
106
|
+
path, so the error and its line number are unchanged.
|
|
107
|
+
- `iters.windowed(step=1)` is staggered `tee` views zipped together, a C-level
|
|
108
|
+
walk: 4x on 10k items. `unique_everseen` skips the key call when there is no
|
|
109
|
+
key, and `last` indexes exact builtin sequences without an ABC probe (2x).
|
|
110
|
+
- Pure forwarders are now the function they forwarded to: `timeutil.utcnow`
|
|
111
|
+
(`partial(datetime.now, UTC)`, 132 → 96 ns), `uid.parse` / `uid.derive`
|
|
112
|
+
(`UUID` / `uuid5`), `iters.flatten` (`chain.from_iterable`, 2x), `zoned.now`
|
|
113
|
+
(`ZonedDateTime.now`), and - with the `text` extra - `fuzz.ratio` /
|
|
114
|
+
`distance` / `jaro_winkler` as the rapidfuzz kernels (~25-40% per call). Clock
|
|
115
|
+
freezers that patch at the C layer (`time-machine`) still see the bound
|
|
116
|
+
references. The pure-Python fallbacks are held to the kernels by a property
|
|
117
|
+
test, since with the extra installed nothing else runs them. The fuzz scorers
|
|
118
|
+
take their two strings positionally.
|
|
119
|
+
- `wavx.wrap_pcm` packs the 44-byte header with one `struct` call - byte-identical
|
|
120
|
+
to `wave`, pinned across widths, channel counts, and partial frames - 2.7x
|
|
121
|
+
faster on 32 KB.
|
|
122
|
+
- `iters.partition` is the current `itertools` recipe - verdicts teed beside the
|
|
123
|
+
values and split with `compress` - so `pred` runs once per element instead of
|
|
124
|
+
once per side: 533 → 459 µs on 10k items with a trivial predicate, and half the
|
|
125
|
+
predicate calls whatever it costs.
|
|
126
|
+
- `ndjson.dumps` joins orjson's row bytes and decodes once, and a text-mode
|
|
127
|
+
`dump` decodes each row's bytes rather than re-entering `jsonx.dumps`: ~20%
|
|
128
|
+
on both (399 → 317 µs and 547 → 421 µs per 1k rows).
|
|
129
|
+
- `clone.asdict` tests dict keys and values for atoms inline instead of paying a
|
|
130
|
+
call per leaf: 5.2 → 3.8 µs on a dict-heavy record.
|
|
131
|
+
- `rand.choices` unboxes the drawn index array once instead of calling `int()`
|
|
132
|
+
per element: 379 → 209 µs for 10k draws.
|
|
133
|
+
|
|
134
|
+
### Fixed
|
|
135
|
+
|
|
136
|
+
- `text.fuzz.best_match` / `extract` / `cdist` keep a choice scoring exactly at
|
|
137
|
+
`score_cutoff`, as documented. rapidfuzz's own cutoff drops boundary pairs
|
|
138
|
+
through a float round-trip (about 15% of random short pairs on 1.1), so every
|
|
139
|
+
fast path now scores in full and applies `>=` itself - at no measurable cost.
|
|
140
|
+
`cdist` also returns float64, so its scores equal `ratio`'s exactly.
|
|
141
|
+
- `fs.atomic_write` / `atomic_writer` no longer flip the process-wide umask to
|
|
142
|
+
read it when creating a new file. Setting it to 0 and back left a window in
|
|
143
|
+
which any other thread creating a file got mode `0o666`/`0o777`. The temp file
|
|
144
|
+
is now opened at `0o666` so the kernel applies the umask, and an existing
|
|
145
|
+
destination's mode is still copied over.
|
|
146
|
+
|
|
147
|
+
- `rex` accepts its own compiled patterns. On the irgx path `rex.compile(p)` and
|
|
148
|
+
`rex.search(p, s)` raised `TypeError` for a `p` this module had just returned,
|
|
149
|
+
because only stdlib's `re.Pattern` was recognized; stdlib takes either.
|
|
150
|
+
- `rex.purge` clears irgx's pattern cache too, not just stdlib's.
|
|
151
|
+
- A pattern irgx declines (backreferences, lookaround) is no longer re-attempted
|
|
152
|
+
on every call. irgx caches what it compiles, not what it refuses, so each
|
|
153
|
+
`rex.search(r"(\w+)\s+\1", ...)` paid a full compile attempt plus a raise -
|
|
154
|
+
about 1.8 ms, against 0.2 µs for stdlib.
|
|
155
|
+
|
|
156
|
+
### Changed
|
|
157
|
+
|
|
158
|
+
- `rex` keeps a bounded front cache like stdlib's `re._cache`, and hands flags to
|
|
159
|
+
irgx as its keyword arguments instead of building an inline `(?imsx-u)` prefix
|
|
160
|
+
through `RegexFlag` arithmetic. A module-level `rex.search` drops from ~5 µs to
|
|
161
|
+
~0.8 µs - now twice as fast as `re.search` - and a compiled pattern's
|
|
162
|
+
`.pattern` is the caller's source again rather than carrying the prefix.
|
|
163
|
+
- `rex.findall` rewrites irgx's `None` for an absent group only when one is
|
|
164
|
+
actually present, found by a C-level containment scan instead of rebuilding
|
|
165
|
+
every row. A two-group `findall` over 10 KB drops from ~940 µs to ~240 µs.
|
|
166
|
+
- The `rex` extra requires `irregex>=2.6.0`. Below it, a replacement template's
|
|
167
|
+
octal escapes (`\07`, `\101`) and unknown non-letter escapes rendered different
|
|
168
|
+
text than stdlib with no error; 2.6.0 reads them the way `re` does. It also
|
|
169
|
+
makes a group-reading `rex.sub` and a grouped `rex.split` one call into the
|
|
170
|
+
engine instead of one per match, and stops a capped `subn`/`split` walking past
|
|
171
|
+
its cap - on 10 KB, templated `sub` goes from ~7x slower than `re.sub` to
|
|
172
|
+
about even, and `subn(..., count=5)` from ~130x to ~1.7x.
|
|
173
|
+
|
|
174
|
+
## [1.1.0] - 2026-09-27
|
|
175
|
+
|
|
176
|
+
### Fixed
|
|
177
|
+
|
|
178
|
+
- `clone.deep` returns the types it was given. Its msgspec round-trip decoded a
|
|
179
|
+
nested `StrEnum` as `str`, an `IntEnum` as `int`, and an `OrderedDict` or
|
|
180
|
+
`defaultdict` as a plain `dict` - losing the factory with it - and the equality
|
|
181
|
+
check meant to catch a lossy round-trip passed every one, because those values
|
|
182
|
+
compare equal to what came back. `deep` is now an exact-type walk that never
|
|
183
|
+
leaves Python objects, so there is nothing to lose.
|
|
184
|
+
- `python3 -m bench update --group X` no longer erases every other group's floors.
|
|
185
|
+
A narrowed run now merges into the baseline instead of replacing it.
|
|
186
|
+
|
|
187
|
+
### Changed
|
|
188
|
+
|
|
189
|
+
- `clone.deep` is pure stdlib and faster where callers actually spend time:
|
|
190
|
+
4.1x over `copy.deepcopy` on a small dict (was 1.2x, slower than deepcopy on
|
|
191
|
+
CPython 3.14), 2.2x on pydantic models (was a failed encode then deepcopy), and
|
|
192
|
+
4.4x on large JSON trees (was 5.8x on CPython 3.13 - the price of the fix above;
|
|
193
|
+
on 3.14 the two tie). Dataclasses, pydantic models, and `msgspec.Struct` are
|
|
194
|
+
rebuilt the way `deepcopy` rebuilds them, with the walker standing in for its
|
|
195
|
+
recursion; any other node is handed to `deepcopy` on its own while its siblings
|
|
196
|
+
keep walking. The `clone` extra no longer installs anything and stays declared
|
|
197
|
+
so existing `unstd[clone]` requirements resolve without a warning.
|
|
198
|
+
|
|
199
|
+
### Added
|
|
200
|
+
|
|
201
|
+
- `clone.asdict`: `dataclasses.asdict`'s result on the same walker, 10x faster on
|
|
202
|
+
a dataclass tree with `datetime` leaves. A custom `dict_factory`, a
|
|
203
|
+
non-dataclass argument, or a namedtuple / container subclass in the tree hands
|
|
204
|
+
the call to the stdlib, so the answer is always the stdlib's.
|
|
205
|
+
|
|
206
|
+
## [1.0.5] - 2026-09-01
|
|
207
|
+
|
|
208
|
+
### Changed
|
|
209
|
+
|
|
210
|
+
- Prose and fixtures name generic things rather than one deployment's. The
|
|
211
|
+
NDJSON docstring described the format by pointing at two artifacts nobody
|
|
212
|
+
outside their origin has ever seen, and a crypto fixture put a private service
|
|
213
|
+
in a JWT audience - so the docstring names what NDJSON is actually used for,
|
|
214
|
+
and the fixture says `example` like its sibling suite already did. No code
|
|
215
|
+
paths move.
|
|
216
|
+
|
|
217
|
+
## [1.0.4] - 2026-08-31
|
|
218
|
+
|
|
219
|
+
### Fixed
|
|
220
|
+
|
|
221
|
+
- The extra floors sit at the minor series rather than the patch we happened to
|
|
222
|
+
develop against, and CI now proves them. 1.0.3 relaxed `==` to `>=` but left
|
|
223
|
+
each floor on an exact patch, so `unstd[model]` still rejected an application
|
|
224
|
+
one pydantic bugfix behind - a resolver error that reads as the application's
|
|
225
|
+
fault. The API a backend offers changes at the minor, which is where a floor
|
|
226
|
+
belongs.
|
|
227
|
+
|
|
228
|
+
### Added
|
|
229
|
+
|
|
230
|
+
- A `floors` CI job resolves `--resolution lowest-direct` and runs the whole
|
|
231
|
+
suite against the oldest version every floor admits. A floor is a promise that
|
|
232
|
+
the library works that far back, and nothing was testing it: the rest of the
|
|
233
|
+
matrix resolves the newest of everything, so the floors could have been fiction
|
|
234
|
+
indefinitely and only a downstream with an older pin would have found out.
|
|
235
|
+
|
|
236
|
+
## [1.0.3] - 2026-08-31
|
|
237
|
+
|
|
238
|
+
### Fixed
|
|
239
|
+
|
|
240
|
+
- Every extra declares a floor rather than an exact `==` pin. The pins were
|
|
241
|
+
inherited from the monorepo this package was extracted from, where an exact
|
|
242
|
+
pin is correct because a monorepo is the application. Published, they meant an
|
|
243
|
+
application already on `pydantic==2.13.4` could not install `unstd[model]` at
|
|
244
|
+
all - the resolver reports the conflict against the application, not against
|
|
245
|
+
us - and two libraries pinning patches are simply not co-installable. The
|
|
246
|
+
floors are the versions each extra is tested against; a consumer that wants an
|
|
247
|
+
exact version has its own manifest and lockfile to say so in.
|
|
248
|
+
|
|
249
|
+
## [1.0.2] - 2026-08-31
|
|
250
|
+
|
|
251
|
+
### Fixed
|
|
252
|
+
|
|
253
|
+
- The package ships `py.typed`. Every surface here is annotated and CI has been
|
|
254
|
+
running `mypy --strict` over all of it, and none of that reached a consumer:
|
|
255
|
+
PEP 561 says to ignore the annotations of a package with no marker, so a
|
|
256
|
+
strict downstream saw `jsonx.dumps` as `Any` and wrote a suppression per call
|
|
257
|
+
site to get its build green.
|
|
258
|
+
|
|
259
|
+
## [1.0.1] - 2026-08-31
|
|
260
|
+
|
|
261
|
+
### Added
|
|
262
|
+
|
|
263
|
+
- `tls.client_context` takes a `minimum_version`, defaulting to the same TLS 1.3
|
|
264
|
+
it always asserted, and `tls.LEGACY_MINIMUM_VERSION` names the 1.2 floor for a
|
|
265
|
+
peer not yet shown to negotiate 1.3. Without it, a service that dials one such
|
|
266
|
+
peer had to hand-roll a second context — which is where the parameter this
|
|
267
|
+
module refuses to have gets added. Anything below 1.2 raises instead of
|
|
268
|
+
building a context.
|
|
269
|
+
|
|
270
|
+
### Fixed
|
|
271
|
+
|
|
272
|
+
- `unstd.__version__` is read from install metadata instead of being a literal
|
|
273
|
+
beside the one in `pyproject.toml`. The two drifted on the first patch release
|
|
274
|
+
and the gate caught its own package's version, so the tag was cut and nothing
|
|
275
|
+
shipped; there is now one place to edit.
|
|
276
|
+
|
|
277
|
+
## [1.0.0] - 2026-08-31
|
|
278
|
+
|
|
279
|
+
### Added
|
|
280
|
+
|
|
281
|
+
- First public release of the full library: serde, ids, crypto, time, model,
|
|
282
|
+
rex, containers, rand, pack, clone, text, audio, toml, proc, fs, and iters.
|
|
283
|
+
Base install stays pure stdlib; every accelerated backend is an optional extra.
|
|
284
|
+
- `crypto.tls` — the verifying TLS client context, with no parameter for
|
|
285
|
+
skipping verification, split out of the module that used to hold it. Its mTLS
|
|
286
|
+
and pinned-root tests now actually run: they mint a throwaway self-signed
|
|
287
|
+
certificate, which needs `cryptography`, which nothing had declared, so all
|
|
288
|
+
three had been skipping rather than passing.
|
|
289
|
+
- `digest.legacy_blake2b` — the one deliberate exception to "one digest",
|
|
290
|
+
for an identifier already defined in terms of BLAKE2b that cannot change
|
|
291
|
+
algorithm without renaming every value derived from it. Keeping that case
|
|
292
|
+
inside the seam is what stops it becoming a reason to import `hashlib` next
|
|
293
|
+
door and grow a second digest for new work too.
|
|
294
|
+
- `ruff` and `mypy --strict` now run in CI, and `release.yml` calls the CI
|
|
295
|
+
workflow as a gate rather than keeping its own copy of one — so the bar a tag
|
|
296
|
+
clears is the bar a pull request clears, by construction. Neither tool had ever
|
|
297
|
+
run over this package; the first pass found 26 type errors.
|
|
298
|
+
- A `proto` extra. `timeutil`'s protobuf `Timestamp` bridge imported
|
|
299
|
+
`google.protobuf` with nothing declaring it, so the two `proto_timestamp`
|
|
300
|
+
functions raised `ModuleNotFoundError` on any install that had not happened to
|
|
301
|
+
pick protobuf up sideways. The import is guarded now and names the extra.
|
|
302
|
+
- `bench` — a harness behind the speedup numbers the READMEs print. Each case is
|
|
303
|
+
one accelerated call against the stdlib call it stands in for, reported as a
|
|
304
|
+
ratio, with `bench/baseline.json` pinning a floor per case that `update` may
|
|
305
|
+
raise and never lower. It refuses to grade a missing backend as a pass, and
|
|
306
|
+
refuses to report a ratio between two functions that disagree on the answer.
|
|
307
|
+
- The suite runs on a base install. It could not before: four surfaces have no
|
|
308
|
+
stdlib fallback and raise at import, which pytest reports as a collection
|
|
309
|
+
error, so `pip install unstd && pytest` aborted before running anything — and
|
|
310
|
+
the guarded-fallback paths, which are the reason the library exists, had never
|
|
311
|
+
been exercised by anything. `tests/conftest.py` now decides collectability by
|
|
312
|
+
probing importability, and the fallback CI leg runs the whole suite (477 tests)
|
|
313
|
+
instead of a hand-maintained subset.
|
|
314
|
+
- Tests for `ndjson`, which had none and 0% coverage; for `timeutil`, which had
|
|
315
|
+
none; and for the package's own description of itself — the top-level docstring
|
|
316
|
+
listed four module groups while sixteen shipped, so the prose, the README table
|
|
317
|
+
and the directory listing are now held to each other, along with the
|
|
318
|
+
zero-dependency claim.
|
|
319
|
+
|
|
320
|
+
### Changed
|
|
321
|
+
|
|
322
|
+
- `crypto` no longer projects an external algorithm table. The widths it
|
|
323
|
+
enforces are BLAKE3's own — 32 bytes of digest, 32 of key, 32 of tag — so they
|
|
324
|
+
live beside the calls they bound (`digest.DIGEST_BYTES` / `KDF_BYTES`,
|
|
325
|
+
`token.KEY_BYTES` / `TAG_BYTES`) instead of being imported from a generated
|
|
326
|
+
registry. `derive_key` takes any context string and says plainly that
|
|
327
|
+
registering one is the caller's job; nothing about the derivation changed.
|
|
328
|
+
- The crypto known-answer vectors are now the BLAKE3 team's own published
|
|
329
|
+
`test_vectors.json`, vendored verbatim and attributed in NOTICE. All three
|
|
330
|
+
modes — plain hash, keyed hash (the MAC), and `derive_key` — are checked
|
|
331
|
+
against the authors' answers across 35 input lengths spanning the chunk and
|
|
332
|
+
subtree boundaries, where the previous fixture carried seven hand-picked rows.
|
|
333
|
+
- `rex` is now backed by [irregex](https://github.com/The-Billy-Company/irregex)
|
|
334
|
+
(`import irgx`) instead of `google-re2`; the extra installs `irregex>=2.3.0`.
|
|
335
|
+
The surface, the stdlib-`re` fidelity contract, and the degrade-to-`re`
|
|
336
|
+
fallback for backreference/lookaround patterns are unchanged.
|
|
337
|
+
- The linear-time guarantee now covers three families that previously degraded to
|
|
338
|
+
backtracking `re`: `re.VERBOSE`, `re.ASCII`, and `bytes` patterns. `bytes`
|
|
339
|
+
patterns compile byte-oriented, so `rb"\w+"` stops at `caf` in `café` exactly
|
|
340
|
+
as stdlib does.
|
|
341
|
+
- Two divergences that were always true of `rex` are now documented and pinned by
|
|
342
|
+
tests rather than left to be discovered: `$` matches the absolute end of the
|
|
343
|
+
text (not the position before a trailing newline, which is Perl's and stdlib's
|
|
344
|
+
rule), and POSIX bracket expressions like `[[:alpha:]]` are a real class to the
|
|
345
|
+
linear engine but a nested-set typo to stdlib.
|
|
346
|
+
|
|
347
|
+
### Fixed
|
|
348
|
+
|
|
349
|
+
- Nine `rex` signatures annotated their `pattern` and `string` parameters as
|
|
350
|
+
`DOTALL` — the regex flag — instead of `S`, the type parameter they declare.
|
|
351
|
+
Ruff's `FURB167` rewrites `re.S` to `re.DOTALL`, marks the fix safe, and had
|
|
352
|
+
spliced its replacement over every PEP 695 `S` in the file. The rule is now
|
|
353
|
+
disabled here, with the reason written down next to it.
|
|
354
|
+
- `timeutil.human_now_tz` ignored the timezone it was passed. It built the right
|
|
355
|
+
instant and then called `.astimezone()` with no argument, which converts back
|
|
356
|
+
to local time, so every caller got local time under another zone's name.
|
|
357
|
+
- `timeutil.iso_fmt` stamped a literal `Z` on whatever it was handed. A
|
|
358
|
+
`datetime` in any other zone came out labelled UTC while still carrying its
|
|
359
|
+
original wall-clock digits — a silent offset, in the format used for the wire.
|
|
360
|
+
Aware datetimes are converted to UTC first now; naive ones are relabelled.
|
|
361
|
+
- `timeutil`'s human format string used `%-d` and `%-I`, which are a glibc/BSD
|
|
362
|
+
extension. `strftime` raises `ValueError` on both under Windows, so every
|
|
363
|
+
human-facing timestamp in the module was a hard failure on one of the three
|
|
364
|
+
platforms the package claims to support.
|
|
365
|
+
- A `bytearray` or `memoryview` pattern now raises `TypeError`, matching what
|
|
366
|
+
stdlib `re` does with one. The backends accept any buffer as a pattern, so
|
|
367
|
+
`rex` was quietly accepting patterns its own twin refuses — being a superset is
|
|
368
|
+
a divergence too, and code written against it broke the moment the extra was
|
|
369
|
+
absent and the `re` fallback took over.
|
|
370
|
+
- `sub` and `subn` now type a callable `repl` as receiving either backend's match
|
|
371
|
+
object, which is what has always happened at runtime. The two are
|
|
372
|
+
interchangeable for everything a replacement function reads (`group`, `groups`,
|
|
373
|
+
`groupdict`, `span`, `expand`, `start`, `end`); only `.pos` and `.endpos` are
|
|
374
|
+
stdlib-only.
|
|
375
|
+
- `findall` is typed `list[S | tuple[S, ...]]` rather than a union of two list
|
|
376
|
+
types, so the element type survives instead of collapsing (typeshed spells the
|
|
377
|
+
same fact `list[Any]`). Runtime results are unchanged.
|
|
378
|
+
|
|
379
|
+
## [0.0.1] - 2026-08-31
|
|
380
|
+
|
|
381
|
+
Parked a good name.
|
|
382
|
+
|
|
383
|
+
### Added
|
|
384
|
+
|
|
385
|
+
- Name reservation on PyPI.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: unstd
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: stdlib-faithful accelerated replacements behind optional extras
|
|
5
5
|
Project-URL: Homepage, https://github.com/The-Billy-Company/unstd
|
|
6
6
|
Project-URL: Repository, https://github.com/The-Billy-Company/unstd
|
|
@@ -24,14 +24,12 @@ Provides-Extra: audio
|
|
|
24
24
|
Requires-Dist: numpy>=2.5; extra == 'audio'
|
|
25
25
|
Requires-Dist: soundfile>=0.14; extra == 'audio'
|
|
26
26
|
Provides-Extra: clone
|
|
27
|
-
Requires-Dist: msgspec>=0.21; extra == 'clone'
|
|
28
27
|
Provides-Extra: containers
|
|
29
28
|
Requires-Dist: immutables>=0.21; extra == 'containers'
|
|
30
29
|
Requires-Dist: sortedcontainers>=2.4; extra == 'containers'
|
|
31
30
|
Provides-Extra: crypto
|
|
32
31
|
Requires-Dist: blake3>=1.0; extra == 'crypto'
|
|
33
32
|
Provides-Extra: ids
|
|
34
|
-
Requires-Dist: blake3>=1.0; extra == 'ids'
|
|
35
33
|
Requires-Dist: uuid-utils>=0.17; extra == 'ids'
|
|
36
34
|
Provides-Extra: model
|
|
37
35
|
Requires-Dist: pydantic-settings>=2.15; extra == 'model'
|
|
@@ -43,7 +41,7 @@ Requires-Dist: protobuf>=7.36; extra == 'proto'
|
|
|
43
41
|
Provides-Extra: rand
|
|
44
42
|
Requires-Dist: numpy>=2.5; extra == 'rand'
|
|
45
43
|
Provides-Extra: rex
|
|
46
|
-
Requires-Dist: irregex>=2.
|
|
44
|
+
Requires-Dist: irregex>=2.6.0; extra == 'rex'
|
|
47
45
|
Provides-Extra: serde
|
|
48
46
|
Requires-Dist: msgspec>=0.21; extra == 'serde'
|
|
49
47
|
Requires-Dist: orjson>=3.12; extra == 'serde'
|
|
@@ -85,15 +83,15 @@ python3 -m pip install 'unstd[serde,ids,crypto]'
|
|
|
85
83
|
| Group | Extra | Backend / fallback |
|
|
86
84
|
|---|---|---|
|
|
87
85
|
| [`serde`](src/unstd/serde/README.md) | `unstd[serde]` | orjson / msgspec / pybase64 → stdlib `json`/`base64` |
|
|
88
|
-
| [`ids`](src/unstd/ids/README.md) | `unstd[ids]` |
|
|
86
|
+
| [`ids`](src/unstd/ids/README.md) | `unstd[ids]` | uuid-utils → `uid` falls back to stdlib UUIDv7 |
|
|
89
87
|
| [`crypto`](src/unstd/crypto/README.md) | `unstd[crypto]` | blake3 (required; hashlib ships BLAKE2, a different algorithm) |
|
|
90
|
-
| [`time`](src/unstd/time/README.md) | `unstd[time]` | `timeutil` is pure stdlib; `
|
|
88
|
+
| [`time`](src/unstd/time/README.md) | `unstd[time]` | `timeutil` is pure stdlib; `zoned` requires `whenever`, and `timeutil`'s protobuf `Timestamp` bridge requires `unstd[proto]` |
|
|
91
89
|
| [`model`](src/unstd/model/README.md) | `unstd[model]` | pydantic (required) |
|
|
92
90
|
| [`rex`](src/unstd/rex/README.md) | `unstd[rex]` | irregex → stdlib `re` |
|
|
93
91
|
| [`containers`](src/unstd/containers/README.md) | `unstd[containers]` | sortedcontainers + immutables (required) |
|
|
94
92
|
| [`rand`](src/unstd/rand/README.md) | `unstd[rand]` | numpy PCG64 → stdlib `random`; `crypto` is always `secrets` |
|
|
95
93
|
| [`pack`](src/unstd/pack/README.md) | `unstd[pack]` | numpy → stdlib `struct` |
|
|
96
|
-
| [`clone`](src/unstd/clone/README.md) |
|
|
94
|
+
| [`clone`](src/unstd/clone/README.md) | — | stdlib; exact-type walk, `copy.deepcopy` per foreign node |
|
|
97
95
|
| [`text`](src/unstd/text/README.md) | `unstd[text]` | rapidfuzz → stdlib `difflib` |
|
|
98
96
|
| [`audio`](src/unstd/audio/README.md) | `unstd[audio]` | soundfile + numpy → stdlib `wave` |
|
|
99
97
|
| [`toml`](src/unstd/toml/README.md) | `unstd[toml]` | read is `tomllib`; write requires tomlkit |
|
|
@@ -101,7 +99,7 @@ python3 -m pip install 'unstd[serde,ids,crypto]'
|
|
|
101
99
|
| [`fs`](src/unstd/fs/README.md) | — | stdlib `os` |
|
|
102
100
|
| [`iters`](src/unstd/iters/README.md) | — | stdlib `itertools` |
|
|
103
101
|
|
|
104
|
-
`proc`, `fs`, and `iters` ship in the base install.
|
|
102
|
+
`clone`, `proc`, `fs`, and `iters` ship in the base install.
|
|
105
103
|
|
|
106
104
|
## Prior art
|
|
107
105
|
|
|
@@ -27,15 +27,15 @@ python3 -m pip install 'unstd[serde,ids,crypto]'
|
|
|
27
27
|
| Group | Extra | Backend / fallback |
|
|
28
28
|
|---|---|---|
|
|
29
29
|
| [`serde`](src/unstd/serde/README.md) | `unstd[serde]` | orjson / msgspec / pybase64 → stdlib `json`/`base64` |
|
|
30
|
-
| [`ids`](src/unstd/ids/README.md) | `unstd[ids]` |
|
|
30
|
+
| [`ids`](src/unstd/ids/README.md) | `unstd[ids]` | uuid-utils → `uid` falls back to stdlib UUIDv7 |
|
|
31
31
|
| [`crypto`](src/unstd/crypto/README.md) | `unstd[crypto]` | blake3 (required; hashlib ships BLAKE2, a different algorithm) |
|
|
32
|
-
| [`time`](src/unstd/time/README.md) | `unstd[time]` | `timeutil` is pure stdlib; `
|
|
32
|
+
| [`time`](src/unstd/time/README.md) | `unstd[time]` | `timeutil` is pure stdlib; `zoned` requires `whenever`, and `timeutil`'s protobuf `Timestamp` bridge requires `unstd[proto]` |
|
|
33
33
|
| [`model`](src/unstd/model/README.md) | `unstd[model]` | pydantic (required) |
|
|
34
34
|
| [`rex`](src/unstd/rex/README.md) | `unstd[rex]` | irregex → stdlib `re` |
|
|
35
35
|
| [`containers`](src/unstd/containers/README.md) | `unstd[containers]` | sortedcontainers + immutables (required) |
|
|
36
36
|
| [`rand`](src/unstd/rand/README.md) | `unstd[rand]` | numpy PCG64 → stdlib `random`; `crypto` is always `secrets` |
|
|
37
37
|
| [`pack`](src/unstd/pack/README.md) | `unstd[pack]` | numpy → stdlib `struct` |
|
|
38
|
-
| [`clone`](src/unstd/clone/README.md) |
|
|
38
|
+
| [`clone`](src/unstd/clone/README.md) | — | stdlib; exact-type walk, `copy.deepcopy` per foreign node |
|
|
39
39
|
| [`text`](src/unstd/text/README.md) | `unstd[text]` | rapidfuzz → stdlib `difflib` |
|
|
40
40
|
| [`audio`](src/unstd/audio/README.md) | `unstd[audio]` | soundfile + numpy → stdlib `wave` |
|
|
41
41
|
| [`toml`](src/unstd/toml/README.md) | `unstd[toml]` | read is `tomllib`; write requires tomlkit |
|
|
@@ -43,7 +43,7 @@ python3 -m pip install 'unstd[serde,ids,crypto]'
|
|
|
43
43
|
| [`fs`](src/unstd/fs/README.md) | — | stdlib `os` |
|
|
44
44
|
| [`iters`](src/unstd/iters/README.md) | — | stdlib `itertools` |
|
|
45
45
|
|
|
46
|
-
`proc`, `fs`, and `iters` ship in the base install.
|
|
46
|
+
`clone`, `proc`, `fs`, and `iters` ship in the base install.
|
|
47
47
|
|
|
48
48
|
## Prior art
|
|
49
49
|
|
|
@@ -92,8 +92,8 @@ The harness earned its keep on the first run:
|
|
|
92
92
|
real value is the shape beside it: handed an ndarray it already owns,
|
|
93
93
|
`pack_array` is a memcpy and wins **159×**, and `unpack_array` returns a
|
|
94
94
|
zero-copy view for **1667×**. Hold your data in an array, not a list.
|
|
95
|
-
- **`
|
|
96
|
-
`hashlib` ships from the BLAKE family, so it is the thing `
|
|
95
|
+
- **`digest.raw`'s honest twin is `blake2b`, not `sha256`.** BLAKE2 is what
|
|
96
|
+
`hashlib` ships from the BLAKE family, so it is the thing `digest.raw` stands in for,
|
|
97
97
|
and BLAKE3 beats it 1.7×. `sha256` is not a fair comparison in either
|
|
98
98
|
direction: every ARMv8 and modern x86 core implements it in silicon, so
|
|
99
99
|
`hashlib.sha256` is a hardware instruction and beats single-threaded BLAKE3
|
|
@@ -101,6 +101,15 @@ The harness earned its keep on the first run:
|
|
|
101
101
|
the algorithm, and it is not something a caller can act on.
|
|
102
102
|
- **`clone.deep` on a small dict is 1.19×, not the 1.8× the README claimed.**
|
|
103
103
|
The floor case is real and the README now says what the harness measures.
|
|
104
|
+
- **The 1.0.5 `clone.deep` floors were set against a clone that dropped types.**
|
|
105
|
+
Its msgspec round-trip turned a nested `StrEnum` into a `str` and a
|
|
106
|
+
`defaultdict` into a `dict`, and its equality guard could not tell. 1.1 replaced
|
|
107
|
+
it with a type-exact walker (4.4× on the large trees instead of 5.8×, 4.1× on
|
|
108
|
+
the small dict instead of 1.2×), and the two large-tree floors were re-pinned
|
|
109
|
+
by hand at the usual 70% of the new measurement. That is the one legitimate way
|
|
110
|
+
a floor comes down: the old number was bought with a bug.
|
|
111
|
+
- **`update --group X` used to erase every other group's floors.** It rewrote the
|
|
112
|
+
baseline from the narrowed run alone. It now merges, and a test pins that.
|
|
104
113
|
|
|
105
114
|
## Adding a case
|
|
106
115
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_": "Speed floors, ratio of stdlib cost to unstd cost. Raised by `python3 -m bench update`, never lowered by it. See bench/README.md.",
|
|
3
|
-
"recorded": "2026-09-
|
|
3
|
+
"recorded": "2026-09-27",
|
|
4
4
|
"host": "Darwin arm64 python3.13",
|
|
5
5
|
"cases": {
|
|
6
|
-
"b64.
|
|
6
|
+
"b64.decode · 128 KiB": {
|
|
7
7
|
"floor": 6.42,
|
|
8
8
|
"measured": 8.94,
|
|
9
9
|
"group": "serde"
|
|
10
10
|
},
|
|
11
|
-
"b64.
|
|
11
|
+
"b64.encode · 128 KiB": {
|
|
12
12
|
"floor": 11.77,
|
|
13
13
|
"measured": 16.52,
|
|
14
14
|
"group": "serde"
|
|
@@ -28,19 +28,29 @@
|
|
|
28
28
|
"measured": 1767.1,
|
|
29
29
|
"group": "pack"
|
|
30
30
|
},
|
|
31
|
+
"clone.asdict · dataclass tree": {
|
|
32
|
+
"floor": 7.55,
|
|
33
|
+
"measured": 10.78,
|
|
34
|
+
"group": "clone"
|
|
35
|
+
},
|
|
36
|
+
"clone.deep · 40 pydantic models": {
|
|
37
|
+
"floor": 1.58,
|
|
38
|
+
"measured": 2.25,
|
|
39
|
+
"group": "clone"
|
|
40
|
+
},
|
|
31
41
|
"clone.deep · 500 records": {
|
|
32
|
-
"floor":
|
|
33
|
-
"measured":
|
|
42
|
+
"floor": 3.16,
|
|
43
|
+
"measured": 4.51,
|
|
34
44
|
"group": "clone"
|
|
35
45
|
},
|
|
36
46
|
"clone.deep · nested (200 items)": {
|
|
37
|
-
"floor":
|
|
38
|
-
"measured":
|
|
47
|
+
"floor": 3.02,
|
|
48
|
+
"measured": 4.22,
|
|
39
49
|
"group": "clone"
|
|
40
50
|
},
|
|
41
51
|
"clone.deep · small dict": {
|
|
42
|
-
"floor":
|
|
43
|
-
"measured":
|
|
52
|
+
"floor": 2.93,
|
|
53
|
+
"measured": 4.16,
|
|
44
54
|
"group": "clone"
|
|
45
55
|
},
|
|
46
56
|
"draw.floats · 100k": {
|
|
@@ -68,7 +78,7 @@
|
|
|
68
78
|
"measured": 41.87,
|
|
69
79
|
"group": "text"
|
|
70
80
|
},
|
|
71
|
-
"
|
|
81
|
+
"digest.raw · 1 MiB": {
|
|
72
82
|
"floor": 1.21,
|
|
73
83
|
"measured": 1.72,
|
|
74
84
|
"group": "ids"
|