fix: deep_set fails loud on a tuple instead of silently replacing it; add deep_gets/deep_sets wildcard verbs
deep_set stepping into or through a tuple used to fall through to the dict branch and
replace the tuple with {}, silently corrupting data on a deep_get/deep_set round-trip
(deep_get traverses tuples read-only). it now raises TypeError - tuples are immutable, so
an in-place update is impossible. also adds deep_gets/deep_sets: '*' path segments for
bulk get (always a list) and bulk set (plain value or fn(current)->new); a '*' passed to
the scalar deep_get/deep_set raises ValueError so the return type is never ambiguous.
Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
@@ -89,13 +89,17 @@ timing.FAST_MODE = True # in test setup
|
||||
|
||||
## paths
|
||||
|
||||
Two pairs of verbs: **single-path** (scalar in/out) and **wildcard** (bulk, always a list).
|
||||
|
||||
### single-path — `deep_get` / `deep_set`
|
||||
|
||||
```python
|
||||
from commons import deep_get, deep_set
|
||||
|
||||
data = {"in": {"this": {"old": {"notation": 42}}}, "items": [{"id": "a"}, {"id": "b"}]}
|
||||
|
||||
deep_get(data, "in.this.old.notation") # 42
|
||||
deep_get(data, "items.1.id") # "b" (numeric segment indexes a list)
|
||||
deep_get(data, "items.1.id") # "b" (numeric segment indexes a list/tuple)
|
||||
deep_get(data, "in.nope.here", "DEF") # "DEF" (missing -> default, no raise)
|
||||
|
||||
deep_set({}, "a.b.c", 9) # {"a": {"b": {"c": 9}}}
|
||||
@@ -105,10 +109,34 @@ deep_set(data, "items.9.id", "X") # raises IndexError (out of range, no
|
||||
# silent mis-store)
|
||||
```
|
||||
|
||||
`deep_set` mirrors `deep_get`'s list indexing: a numeric segment over an existing
|
||||
list/tuple updates that element in place rather than replacing the list with a dict.
|
||||
An out-of-range numeric segment raises `IndexError` instead of silently corrupting
|
||||
the structure.
|
||||
`deep_get` steps into both lists and tuples on a numeric segment. `deep_set` updates a
|
||||
**list** element in place; setting into (or through) a **tuple** raises `TypeError` —
|
||||
tuples are immutable, so "update in place" is impossible; flatten to a list first. An
|
||||
out-of-range numeric segment raises `IndexError` instead of silently corrupting the
|
||||
structure.
|
||||
|
||||
### wildcard — `deep_gets` / `deep_sets`
|
||||
|
||||
A `*` segment iterates every element at that level (dict values or list/tuple items);
|
||||
multiple `*` fan out cartesian. These are **separate verbs** with a fixed list/bulk
|
||||
return type — a `*` passed to `deep_get`/`deep_set` raises `ValueError` (use the plural
|
||||
verb).
|
||||
|
||||
```python
|
||||
from commons import deep_gets, deep_sets
|
||||
|
||||
data = {"users": [{"username": "al"}, {"username": "bo"}, {"username": "cy"}]}
|
||||
|
||||
deep_gets(data, "users.*.username") # ["al", "bo", "cy"] (ALWAYS a list, in order)
|
||||
deep_gets(data, "users.*.nope") # [] (no match -> empty list, no raise)
|
||||
|
||||
deep_sets(data, "users.*.username", "X") # set every match to "X"
|
||||
deep_sets(data, "users.*.username", str.upper) # callable fn(current)->new per match
|
||||
```
|
||||
|
||||
`deep_sets`'s second arg is either a plain value (write it to every match) or a callable
|
||||
`fn(current) -> new` (compute each). It writes only matches that already exist (it does
|
||||
not create missing keys). Setting through/into a tuple raises `TypeError`.
|
||||
|
||||
## masking
|
||||
|
||||
|
||||
Reference in New Issue
Block a user