dotted-notation 0.46.1__tar.gz → 0.47.1__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.
Files changed (48) hide show
  1. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/CHANGELOG.md +60 -0
  2. {dotted_notation-0.46.1/dotted_notation.egg-info → dotted_notation-0.47.1}/PKG-INFO +171 -35
  3. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/README.md +170 -34
  4. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/access.pxd +3 -3
  5. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/access.py +25 -18
  6. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/api.py +23 -21
  7. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/base.pxd +6 -0
  8. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/base.py +47 -3
  9. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/engine.pxd +3 -3
  10. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/engine.py +13 -11
  11. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/filters.pxd +4 -4
  12. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/filters.py +68 -64
  13. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/grammar.py +140 -183
  14. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/groups.pxd +1 -1
  15. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/groups.py +51 -1
  16. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/matchers.py +8 -6
  17. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/recursive.py +3 -3
  18. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/results.py +93 -59
  19. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/sql/pg.py +14 -5
  20. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/transforms.py +18 -0
  21. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/wrappers.pxd +5 -5
  22. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/wrappers.py +255 -75
  23. {dotted_notation-0.46.1 → dotted_notation-0.47.1/dotted_notation.egg-info}/PKG-INFO +171 -35
  24. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/pyproject.toml +1 -1
  25. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/LICENSE +0 -0
  26. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/MANIFEST.in +0 -0
  27. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/__init__.py +0 -0
  28. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/__main__.py +0 -0
  29. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/cli/__init__.py +0 -0
  30. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/cli/_compat.py +0 -0
  31. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/cli/formats.py +0 -0
  32. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/cli/main.py +0 -0
  33. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/containers.py +0 -0
  34. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/matchers.pxd +0 -0
  35. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/native.py +0 -0
  36. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/predicates.py +0 -0
  37. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/recursive.pxd +0 -0
  38. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/sql/__init__.py +0 -0
  39. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/sql/core.py +0 -0
  40. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/utils.py +0 -0
  41. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted/utypes.py +0 -0
  42. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted_notation.egg-info/SOURCES.txt +0 -0
  43. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted_notation.egg-info/dependency_links.txt +0 -0
  44. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted_notation.egg-info/entry_points.txt +0 -0
  45. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted_notation.egg-info/requires.txt +0 -0
  46. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/dotted_notation.egg-info/top_level.txt +0 -0
  47. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/setup.cfg +0 -0
  48. {dotted_notation-0.46.1 → dotted_notation-0.47.1}/setup.py +0 -0
@@ -3,6 +3,66 @@
3
3
  All notable changes to `dotted` are recorded here. Versions prior to
4
4
  the ones listed are omitted — browse git history for earlier entries.
5
5
 
6
+ ## [0.47.1]
7
+
8
+ ### Added
9
+ - The `coalesce` transform: `n|coalesce:0` is `n`, or 0 when `n` is
10
+ `None`. Several fallbacks take the first that is not `None`, so a
11
+ reference may stand in first: `n|coalesce:$$(defaults.n):0`.
12
+
13
+ ## [0.47.0]
14
+
15
+ ### Added
16
+ - References resolve in transform arguments, guard values and filter
17
+ values: `n|add:$$(config.offset)`, `*>$$(limit)`,
18
+ `users[*&name=$$(selected)]`. `$$(path)` is the root as before;
19
+ `$$(^path)` is the value being transformed or compared, `$$(^^path)`
20
+ the node holding it, and so on up. A reference that does not resolve
21
+ matches nothing, as in an access position.
22
+
23
+ ### Fixed
24
+ - A `$(name)` substitution in a transform argument resolved through
25
+ `bindings=` is handed to the transform as its value; it was handed as
26
+ the parsed wrapper, so the transform failed and returned the value
27
+ unchanged. `null`, `true` and `false` arguments are likewise unwrapped.
28
+ - A reference as a guard or filter value raised `AttributeError`.
29
+ - A value guard after an attribute access, `o@name=5`, or after a group,
30
+ `(a,b)=5`, raised `ValueError` when parsed.
31
+ - A value guard under `~` did not hold for writes: `~a=1` raised
32
+ `TypeError` on `update` and `remove`, and `[~0]=1` mutated.
33
+ - `!a.b` yielded the complement of `a` and dropped the `.b`; it now
34
+ continues into `b` from each key other than `a`.
35
+
36
+ ### Changed
37
+ - Precedence, tightest first: access ops and `:type`; `!`; then guards,
38
+ filters and transforms; then `&`; then `,`. A guard, filter or
39
+ transform applies to the whole expression on its left. After a
40
+ parenthesized `,`, `&` or `?` group it is shorthand for writing it on
41
+ every branch: `(a,b)=5` is `(a=5,b=5)`, `(first&last)>5` is
42
+ `(first>5&last>5)`, `(a,b)|int` is `(a|int,b|int)`, which is also how
43
+ `assemble` writes them. After `!` it applies to what the negation
44
+ yields: `!a=5` is `(!a)=5`, the non-`a` values equal to 5, where it was
45
+ `!(a=5)`; that reading now needs the parentheses. `a,b|int` transforms
46
+ only `b`. `@attr=value` is a value guard like `key=value`, honored by
47
+ `update` and `remove`.
48
+ - `:type` tests the value a path yields, not the node it is applied to:
49
+ `x:int` is `x`, when its value is an `int`, a type guard beside `=value`
50
+ at the same level, so `(a|int):int` types a transformed value and
51
+ `*:dict` is the values that are dicts. It tested the container, so
52
+ `x:int` on `{'x': 3}` was `None`. A restriction on the container moves
53
+ one segment earlier: old `a.b:dict` is new `a:dict.b`, and
54
+ `*[*]:!(str, bytes)` is `*:!(str, bytes)[*]`.
55
+ - Every op owns its transforms: `a.b|int` carries `int` on `b`, so a
56
+ transform can sit inside a group and `expand`, `pluck` and `unpack`
57
+ write it back on each concrete path. `Dotted.transforms` is the
58
+ trailing op's, read-only. The match-only transforms of a guard or
59
+ filter, `a|int=5`, are its `guard_transforms`.
60
+ - For development: the `.pxd` files cimport `dotted.base` by its full
61
+ name, which the compiler's dependency scanner follows, so a change to
62
+ `base.pxd` rebuilds every compiled module that depends on it. Written
63
+ as `from . cimport base`, the dependency went unseen and stale modules
64
+ warned about a changed `DepthStack` size.
65
+
6
66
  ## [0.46.1]
7
67
 
8
68
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.46.1
3
+ Version: 0.47.1
4
4
  Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
5
5
  Author-email: Frey Waid <logophage1@gmail.com>
6
6
  License-Expression: MIT
@@ -144,6 +144,7 @@ For faster `mutable=False` updates and removes, install the
144
144
  - [Template bindings](#template-bindings)
145
145
  - [References](#references)
146
146
  - [Relative References](#relative-references)
147
+ - [References in transforms and guards](#references-in-transforms-and-guards)
147
148
  - [Escaping](#escaping)
148
149
  - [Type Restrictions](#type-restrictions)
149
150
  - [Positive restrictions](#positive-restrictions)
@@ -272,6 +273,32 @@ Several Python libraries handle nested data access. Here's how dotted compares:
272
273
  <a id="breaking-changes"></a>
273
274
  ## Breaking Changes
274
275
 
276
+ ### v0.47.0
277
+ - **`!` binds tighter than guards, filters and transforms.** `!a=5` is now
278
+ `(!a)=5`, the values other than `a`'s that equal 5; the old meaning,
279
+ everything but an `a` equal to 5, is written `!(a=5)`. Likewise `!a&k=v`
280
+ filters what `!a` yields and `!a|int` transforms it. See
281
+ [Precedence](#precedence).
282
+ - **A guard, filter or transform after a group distributes to every branch.**
283
+ `(a,b)=5` is `(a=5,b=5)`, `(first&last)>5` is `(first>5&last>5)`,
284
+ `(a,b)|int` is `(a|int,b|int)`, and `assemble` writes the distributed form.
285
+ These used to fail to parse.
286
+ - **`a,b|int` transforms only `b`.** A transform binds to the expression on
287
+ its left before `,` and `&`; `(a,b)|int` transforms both.
288
+ - **`:type` tests the value a path yields, not the node it is applied to.**
289
+ `x:int` on `{'x': 3}` is `3`; it was `None`. Restrict a container one
290
+ segment earlier: `a.b:dict` → `a:dict.b`, and
291
+ `*[*]:!(str, bytes)` → `*:!(str, bytes)[*]`. `**:!(str, bytes)` and
292
+ `*(*, [*]):!(str, bytes)` now drop string values themselves, not only
293
+ their decomposition.
294
+ - **`!a.b` continues into `b`.** It yielded the complement of `a` and
295
+ ignored `.b`; it now yields `.b` of every key other than `a`, as
296
+ `match` always read it.
297
+ - **Ops own their transforms.** `Dotted.transforms` is read-only and
298
+ reflects the trailing op; `parse('a.b|int')` reprs as
299
+ `Dotted([a, b|int])`. A guard's or filter's match-only transforms,
300
+ `a|int=5`, are `guard_transforms`, not `transforms`.
301
+
275
302
  ### v0.44.0
276
303
  - **Parameter renamed from `key` to `path`** across the public API to align with
277
304
  the terminology used throughout the docs (a _path_ is the whole dotted
@@ -1374,6 +1401,7 @@ at replace time) and **references** (resolved during traversal).
1374
1401
  | `$$(path)` | Reference | Root object during traversal |
1375
1402
  | `$$(^path)` | Relative reference | Current node during traversal |
1376
1403
  | `$$(^^path)` | Relative reference | Parent node during traversal |
1404
+ | `$$(^path)` in a transform, guard or filter | Relative reference | The value under test; `^^` the node holding it |
1377
1405
 
1378
1406
  <a id="substitution"></a>
1379
1407
  ### Substitution
@@ -1559,6 +1587,50 @@ Relative references combine with patterns:
1559
1587
  >>> dotted.get(data, 'a.$$(^config.*.field)')
1560
1588
  ('Alice', 30)
1561
1589
 
1590
+ <a id="references-in-transforms-and-guards"></a>
1591
+ ### References in transforms and guards
1592
+
1593
+ A reference can also stand in for a transform argument, a guard value or a
1594
+ filter value. It resolves when the transform or comparison runs, against the
1595
+ value under test: `$$(path)` is the root as always, `$$(^path)` is the value
1596
+ itself, `$$(^^path)` the node holding it, `$$(^^^path)` that node's parent,
1597
+ and so on:
1598
+
1599
+ >>> data = {'config': {'offset': 10}, 'n': 5}
1600
+ >>> dotted.get(data, 'n|add:$$(config.offset)')
1601
+ 15
1602
+
1603
+ >>> data = {'items': [{'price': 100, 'rate': 1.5}, {'price': 20, 'rate': 2}]}
1604
+ >>> dotted.get(data, 'items[*].price|mul:$$(^^rate)')
1605
+ (150.0, 40)
1606
+
1607
+ Each `price` is the value being transformed, so `^^rate` is the `rate` beside
1608
+ it. Guard and filter values resolve the same way:
1609
+
1610
+ >>> data = {'limit': 10, 'a': 5, 'b': 15}
1611
+ >>> dotted.get(data, '*>$$(limit)')
1612
+ (15,)
1613
+ >>> data = {'selected': 'bob', 'users': [{'name': 'alice'}, {'name': 'bob'}]}
1614
+ >>> dotted.get(data, 'users[*&name=$$(selected)].name')
1615
+ ('bob',)
1616
+
1617
+ as do the transforms before a guard:
1618
+
1619
+ >>> data = {'scale': 10, 'a': 1, 'b': 2}
1620
+ >>> dotted.get(data, '*|mul:$$(scale)=20')
1621
+ (2,)
1622
+
1623
+ With `update`, the transforms apply to the value being set, so `^` is that
1624
+ value:
1625
+
1626
+ >>> dotted.update({'n': 5, 'inc': 3}, 'n|add:$$(inc)', 10)
1627
+ {'n': 13, 'inc': 3}
1628
+
1629
+ A reference that does not resolve matches nothing, as in an access position:
1630
+
1631
+ >>> dotted.get({'a': 1}, 'a|add:$$(missing)', default='fallback')
1632
+ 'fallback'
1633
+
1562
1634
  <a id="escaping"></a>
1563
1635
  ### Escaping
1564
1636
 
@@ -1586,17 +1658,19 @@ To use a literal `$$` as a key, escape with backslash (`\$$`) or quote it
1586
1658
  <a id="type-restrictions"></a>
1587
1659
  ## Type Restrictions
1588
1660
 
1589
- A **type restriction** constrains a path segment so it only applies when the
1590
- current node is a specific type. Append `:type` after any accessor — key,
1591
- slot, or attr:
1661
+ A **type restriction** keeps a value only if it is of a given type. Append
1662
+ `:type` to a path: `x:int` is `x`, when its value is an `int`, so it reads
1663
+ like a guard, `x=5`, with a type in place of a value:
1592
1664
 
1593
1665
  >>> import dotted
1594
1666
  >>> d = {'a': {'b': 1}, 'c': [1, 2], 'd': 'hello'}
1667
+ >>> dotted.get(d, '*:dict')
1668
+ ({'b': 1},)
1595
1669
  >>> dotted.get(d, '*:dict.*')
1596
1670
  (1,)
1597
1671
 
1598
- Here `*:dict` matches all keys but only descends into values that are dicts.
1599
- The list and string are skipped.
1672
+ Here `*:dict` keeps the values that are dicts, and `.*` continues into them.
1673
+ The list and string are dropped.
1600
1674
 
1601
1675
  Supported types: `str`, `bytes`, `int`, `float`, `dict`, `list`, `tuple`,
1602
1676
  `set`, `frozenset`, `bool`.
@@ -1626,17 +1700,16 @@ Supported types: `str`, `bytes`, `int`, `float`, `dict`, `list`, `tuple`,
1626
1700
 
1627
1701
  Type restrictions work on slot and attr accessors too:
1628
1702
 
1629
- >>> dotted.get([10, 20], '[0]:list')
1630
- 10
1631
- >>> dotted.get((10, 20), '[0]:list', 'missing')
1703
+ >>> dotted.get([10, 'x', 20], '[*]:int')
1704
+ (10, 20)
1705
+ >>> dotted.get([10, 20], '[0]:list', 'missing')
1632
1706
  'missing'
1633
- >>> dotted.get((10, 20), '[0]:tuple')
1634
- 10
1635
1707
 
1636
- Use `:!(str, bytes)` on slot wildcards to prevent string and bytes decomposition:
1708
+ A string or bytes value decomposes under `[*]`; to keep that from happening,
1709
+ restrict the value before the slot:
1637
1710
 
1638
1711
  >>> d = {'s': 'abc', 'b': b'xyz', 'l': [1, 2]}
1639
- >>> dotted.get(d, '*[*]:!(str, bytes)')
1712
+ >>> dotted.get(d, '*:!(str, bytes)[*]')
1640
1713
  (1, 2)
1641
1714
 
1642
1715
  <a id="combining-with-filters"></a>
@@ -1648,11 +1721,23 @@ Type restrictions compose with filters. The restriction goes before the filter:
1648
1721
  >>> dotted.get(d, '*:dict&x=1.*')
1649
1722
  (1, 2)
1650
1723
 
1724
+ <a id="typing-a-transformed-value"></a>
1725
+ ### Typing a transformed value
1726
+
1727
+ A restriction after a parenthesized group applies to what the group yields,
1728
+ transforms included. A `:` directly after a transform name is a parameter,
1729
+ `a|int:16`, so the parentheses are what make it a type:
1730
+
1731
+ >>> dotted.get({'a': '5', 'b': 'x'}, '(*|int):int')
1732
+ (5,)
1733
+
1734
+ Here `|int` leaves `'x'` as it was, and `:int` drops it.
1735
+
1651
1736
  <a id="recursive-traversal-with-type-restrictions"></a>
1652
1737
  ### Recursive traversal with type restrictions
1653
1738
 
1654
- Type restrictions are especially useful in recursive accessor groups to control
1655
- which containers are traversed:
1739
+ Type restrictions are especially useful in recursive accessor groups to keep
1740
+ the traversal to values of certain types:
1656
1741
 
1657
1742
  >>> d = {'a': {'b': [1, 2]}, 'c': 'hello'}
1658
1743
  >>> dotted.get(d, '*(*#, [*]:!(str, bytes))')
@@ -1662,11 +1747,12 @@ Without `:!(str, bytes)`, the `[*]` slot accessor would decompose `'hello'`
1662
1747
  into individual characters.
1663
1748
 
1664
1749
  A type restriction can also be applied to an entire group, which distributes it
1665
- to every accessor in the group:
1750
+ to every accessor in the group. Here it drops the string `'hello'` as well,
1751
+ since the `*` accessor yields it too:
1666
1752
 
1667
1753
  >>> d = {'a': {'b': [1, 2]}, 'c': 'hello'}
1668
1754
  >>> dotted.get(d, '*(*#, [*]):!(str, bytes)')
1669
- ({'b': [1, 2]}, [1, 2], 1, 2, 'hello')
1755
+ ({'b': [1, 2]}, [1, 2], 1, 2)
1670
1756
 
1671
1757
  The `**` shorthand accepts a type restriction too. It can be combined with a
1672
1758
  depth slice — the type restriction comes first:
@@ -2017,27 +2103,45 @@ Updates and removes apply to all non-matching keys:
2017
2103
  <a id="precedence"></a>
2018
2104
  ### Precedence
2019
2105
 
2020
- Access ops (`.`, `@`, `[]`) bind tightest—they build compound paths
2021
- before any operator sees them. Then `!`, then `&`, then `,`:
2106
+ Tightest first:
2107
+
2108
+ | Level | Operators | Example |
2109
+ |-------|-----------|---------|
2110
+ | 1 | access ops `.` `@` `[]`, parentheses | `!a.b` is `!(a.b)` |
2111
+ | 2 | `!` negation | `!a=5` is `(!a)=5` |
2112
+ | 3 | `:type`, `\|transform`, guard `=value`, filter `&key=value` | `!a\|int=5` tests `int` of each `(!a)` value |
2113
+ | 4 | `&` conjunction | `!a=5&b` is `((!a)=5)&b` |
2114
+ | 5 | `,` disjunction | `a,b\|int` is `a,(b\|int)` |
2022
2115
 
2023
- | Precedence | Operator | Example |
2024
- |------------|----------|---------|
2025
- | tightest | `.` `@` `[]` (access ops) | `a.b` is one path |
2026
- | | `!` (negation) | `!a.b` = `!(a.b)` |
2027
- | | `&` (conjunction) | `!a&b` = `(!a)&b` |
2028
- | loosest | `,` (disjunction) | `!a&b,c` = `((!a)&b),c` |
2116
+ A guard, filter or transform applies to the whole expression on its left:
2117
+ an access path (on its last segment), a `!` expression (on what it
2118
+ yields), or a parenthesized group, where it is shorthand for writing it on
2119
+ every branch: `(a,b)=5` is `(a=5,b=5)` and `(a,b)|int` is `(a|int,b|int)`,
2120
+ which is also how `assemble` writes them.
2029
2121
 
2030
2122
  >>> d = {'a': 1, 'b': 2, 'c': 3}
2031
- >>> dotted.get(d, '!a&b,c') # parsed as ((!a)&b), c
2123
+ >>> dotted.get(d, '!a&b,c') # ((!a)&b), c
2032
2124
  (2, 3, 2, 3)
2125
+ >>> dotted.get(d, '!a=3') # (!a)=3: the values other than a's equal to 3
2126
+ (3,)
2127
+ >>> dotted.get(d, '!(a=3)') # everything but an a equal to 3
2128
+ (1, 2, 3)
2129
+ >>> dotted.get({'a': '1', 'b': '2'}, '(a,b)|int')
2130
+ (1, 2)
2131
+ >>> dotted.get({'a': '1', 'b': '2'}, 'a,b|int')
2132
+ ('1', 2)
2033
2133
 
2034
- Since `.` binds tightest, `!a.b&c` is `(!a.b) & c`, not `!(a.b&c)`:
2134
+ Since access ops bind tightest, `!a.b` negates `a` and continues into `b`
2135
+ from every other key, and `!a.b&x` conjoins that with `x`:
2035
2136
 
2036
- >>> d = {'a': {'b': 1}, 'c': 2, 'x': 3}
2037
- >>> dotted.get(d, '!a.b&c') # (!a.b)&c — negate a.b, conjoin with c
2038
- (2, 3, 2)
2039
- >>> dotted.get(d, '!(a.b&c)') # negate the conjunction as a unit
2040
- (3,)
2137
+ >>> d = {'a': {'b': 1}, 'c': {'b': 2}, 'x': 3}
2138
+ >>> dotted.get(d, '!a.b')
2139
+ (2,)
2140
+ >>> dotted.get(d, '!a.b&x')
2141
+ (2, 3)
2142
+
2143
+ A `:` after a transform name is always a parameter, `a|int:16`; to type a
2144
+ transformed value, parenthesize: `(a|int):int`.
2041
2145
 
2042
2146
  <a id="operators"></a>
2043
2147
  ## Operators
@@ -2416,8 +2520,8 @@ Filters support `True`, `False`, and `None` as values:
2416
2520
  ### Value guard
2417
2521
 
2418
2522
  A **value guard** tests the value at a path and yields it only if it matches.
2419
- Use `key=value` or `[slot]=value` after accessing a field. All comparison
2420
- operators (`=`, `!=`, `<`, `>`, `<=`, `>=`) are supported:
2523
+ Use `key=value`, `[slot]=value` or `@attr=value` after accessing a field. All
2524
+ comparison operators (`=`, `!=`, `<`, `>`, `<=`, `>=`) are supported:
2421
2525
 
2422
2526
  >>> d = {'first': 7, 'last': 3}
2423
2527
  >>> dotted.get(d, 'first=7')
@@ -2436,6 +2540,19 @@ For lists of primitive values, use `[*]=value`:
2436
2540
  >>> dotted.get(data, '[0]=1')
2437
2541
  1
2438
2542
 
2543
+ A guard after a parenthesized group is shorthand for a guard on every
2544
+ branch: `(first,last)>5` is `(first>5,last>5)`, and `(first&last)>5` is
2545
+ `(first>5&last>5)`, which yields nothing unless both pass. After `!` it
2546
+ tests what the negation yields, so `!first=3` is the non-`first` values
2547
+ equal to 3; see [Precedence](#precedence).
2548
+
2549
+ >>> dotted.get(d, '(first,last)>5')
2550
+ (7,)
2551
+ >>> dotted.get(d, '(first&last)>5')
2552
+ ()
2553
+ >>> dotted.get(d, '!first=3')
2554
+ (3,)
2555
+
2439
2556
  Guards support all value types: numbers, `None`, `True`/`False`, strings, regex, `*`,
2440
2557
  and [container patterns](#container-filter-values) (`[1, ...]`, `{"a": 1, ...: *}`, `{1, 2, ...}`):
2441
2558
 
@@ -2523,6 +2640,9 @@ Guard transforms compose with `update`, `remove`, and `has`:
2523
2640
  >>> dotted.has({'val': '7'}, 'val|int=7')
2524
2641
  True
2525
2642
 
2643
+ Both the transform arguments and the value compared with may be
2644
+ [references](#references-in-transforms-and-guards).
2645
+
2526
2646
  <a id="container-filter-values"></a>
2527
2647
  ### Container filter values
2528
2648
 
@@ -2838,7 +2958,9 @@ Filter keys can include slice notation so the comparison applies to a slice of t
2838
2958
  You can optionally add transforms to the end of dotted notation. These will
2839
2959
  be applied on `get` and `update`. Transforms are separated by the `|` operator
2840
2960
  and multiple may be chained together. Transforms may be parameterized using
2841
- the `:` operator.
2961
+ the `:` operator; a parameter may be a `$$(reference)`, resolved when the
2962
+ transform runs (see
2963
+ [References in transforms and guards](#references-in-transforms-and-guards)).
2842
2964
 
2843
2965
  >>> import dotted
2844
2966
  >>> d = [1, '2', 3]
@@ -2862,6 +2984,7 @@ decorator.
2862
2984
  | `float` | `raises` | Convert to float |
2863
2985
  | `decimal` | `raises` | Convert to `Decimal` |
2864
2986
  | `none` | values... | Return `None` if falsy or matches values: `\|none::null:empty` |
2987
+ | `coalesce` | fallbacks... | The first of the value and the fallbacks that is not `None`: `\|coalesce:0` |
2865
2988
  | `strip` | `chars`, `raises` | Strip whitespace or specified chars |
2866
2989
  | `len` | `default` | Get length. Optional default if not sized: `\|len:0` |
2867
2990
  | `lowercase` | `raises` | Convert string to lowercase |
@@ -2916,6 +3039,19 @@ Comparison and membership transforms return `True`/`False`:
2916
3039
  >>> dotted.get({'n': 2}, 'n|in:[1, 2, 3]')
2917
3040
  True
2918
3041
 
3042
+ `coalesce` stands in a fallback for `None`, as SQL's does; with several
3043
+ fallbacks the first that is not `None` wins, which matters when a fallback is
3044
+ a [reference](#references-in-transforms-and-guards) that may itself be `None`:
3045
+
3046
+ >>> d = {'n': None, 'defaults': {'n': 7}}
3047
+ >>> dotted.get(d, 'n|coalesce:0')
3048
+ 0
3049
+ >>> dotted.get(d, 'n|coalesce:$$(defaults.n):0')
3050
+ 7
3051
+
3052
+ A transform only runs on a value the path yields, so `coalesce` does not fill
3053
+ a missing key; `get(..., default=)` does that.
3054
+
2919
3055
  <a id="container-transform-arguments"></a>
2920
3056
  ### Container transform arguments
2921
3057