dotted-notation 0.45.1__tar.gz → 0.47.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.
Files changed (49) hide show
  1. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/CHANGELOG.md +89 -0
  2. dotted_notation-0.47.0/MANIFEST.in +4 -0
  3. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/PKG-INFO +137 -39
  4. dotted_notation-0.45.1/dotted_notation.egg-info/PKG-INFO → dotted_notation-0.47.0/README.md +133 -82
  5. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/__init__.py +6 -0
  6. dotted_notation-0.47.0/dotted/access.pxd +57 -0
  7. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/access.py +141 -107
  8. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/api.py +25 -22
  9. dotted_notation-0.47.0/dotted/base.pxd +76 -0
  10. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/base.py +104 -9
  11. dotted_notation-0.47.0/dotted/engine.pxd +18 -0
  12. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/engine.py +45 -29
  13. dotted_notation-0.47.0/dotted/filters.pxd +60 -0
  14. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/filters.py +68 -64
  15. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/grammar.py +287 -191
  16. dotted_notation-0.47.0/dotted/groups.pxd +34 -0
  17. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/groups.py +87 -173
  18. dotted_notation-0.47.0/dotted/matchers.pxd +87 -0
  19. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/matchers.py +14 -11
  20. dotted_notation-0.47.0/dotted/native.py +59 -0
  21. dotted_notation-0.47.0/dotted/recursive.pxd +24 -0
  22. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/recursive.py +32 -32
  23. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/results.py +93 -59
  24. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/sql/pg.py +14 -5
  25. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/utils.py +8 -10
  26. dotted_notation-0.47.0/dotted/wrappers.pxd +27 -0
  27. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/wrappers.py +287 -109
  28. dotted_notation-0.45.1/README.md → dotted_notation-0.47.0/dotted_notation.egg-info/PKG-INFO +180 -36
  29. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/SOURCES.txt +10 -0
  30. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/requires.txt +1 -0
  31. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/pyproject.toml +16 -6
  32. dotted_notation-0.47.0/setup.py +53 -0
  33. dotted_notation-0.45.1/MANIFEST.in +0 -2
  34. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/LICENSE +0 -0
  35. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/__main__.py +0 -0
  36. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/cli/__init__.py +0 -0
  37. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/cli/_compat.py +0 -0
  38. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/cli/formats.py +0 -0
  39. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/cli/main.py +0 -0
  40. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/containers.py +0 -0
  41. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/predicates.py +0 -0
  42. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/sql/__init__.py +0 -0
  43. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/sql/core.py +0 -0
  44. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/transforms.py +0 -0
  45. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted/utypes.py +0 -0
  46. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/dependency_links.txt +0 -0
  47. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/entry_points.txt +0 -0
  48. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/top_level.txt +0 -0
  49. {dotted_notation-0.45.1 → dotted_notation-0.47.0}/setup.cfg +0 -0
@@ -3,6 +3,95 @@
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.0]
7
+
8
+ ### Added
9
+ - References resolve in transform arguments, guard values and filter
10
+ values: `n|add:$$(config.offset)`, `*>$$(limit)`,
11
+ `users[*&name=$$(selected)]`. `$$(path)` is the root as before;
12
+ `$$(^path)` is the value being transformed or compared, `$$(^^path)`
13
+ the node holding it, and so on up. A reference that does not resolve
14
+ matches nothing, as in an access position.
15
+
16
+ ### Fixed
17
+ - A `$(name)` substitution in a transform argument resolved through
18
+ `bindings=` is handed to the transform as its value; it was handed as
19
+ the parsed wrapper, so the transform failed and returned the value
20
+ unchanged. `null`, `true` and `false` arguments are likewise unwrapped.
21
+ - A reference as a guard or filter value raised `AttributeError`.
22
+ - A value guard after an attribute access, `o@name=5`, or after a group,
23
+ `(a,b)=5`, raised `ValueError` when parsed.
24
+ - A value guard under `~` did not hold for writes: `~a=1` raised
25
+ `TypeError` on `update` and `remove`, and `[~0]=1` mutated.
26
+ - `!a.b` yielded the complement of `a` and dropped the `.b`; it now
27
+ continues into `b` from each key other than `a`.
28
+
29
+ ### Changed
30
+ - Precedence, tightest first: access ops and `:type`; `!`; then guards,
31
+ filters and transforms; then `&`; then `,`. A guard, filter or
32
+ transform applies to the whole expression on its left. After a
33
+ parenthesized `,`, `&` or `?` group it is shorthand for writing it on
34
+ every branch: `(a,b)=5` is `(a=5,b=5)`, `(first&last)>5` is
35
+ `(first>5&last>5)`, `(a,b)|int` is `(a|int,b|int)`, which is also how
36
+ `assemble` writes them. After `!` it applies to what the negation
37
+ yields: `!a=5` is `(!a)=5`, the non-`a` values equal to 5, where it was
38
+ `!(a=5)`; that reading now needs the parentheses. `a,b|int` transforms
39
+ only `b`. `@attr=value` is a value guard like `key=value`, honored by
40
+ `update` and `remove`.
41
+ - `:type` tests the value a path yields, not the node it is applied to:
42
+ `x:int` is `x`, when its value is an `int`, a type guard beside `=value`
43
+ at the same level, so `(a|int):int` types a transformed value and
44
+ `*:dict` is the values that are dicts. It tested the container, so
45
+ `x:int` on `{'x': 3}` was `None`. A restriction on the container moves
46
+ one segment earlier: old `a.b:dict` is new `a:dict.b`, and
47
+ `*[*]:!(str, bytes)` is `*:!(str, bytes)[*]`.
48
+ - Every op owns its transforms: `a.b|int` carries `int` on `b`, so a
49
+ transform can sit inside a group and `expand`, `pluck` and `unpack`
50
+ write it back on each concrete path. `Dotted.transforms` is the
51
+ trailing op's, read-only. The match-only transforms of a guard or
52
+ filter, `a|int=5`, are its `guard_transforms`.
53
+ - For development: the `.pxd` files cimport `dotted.base` by its full
54
+ name, which the compiler's dependency scanner follows, so a change to
55
+ `base.pxd` rebuilds every compiled module that depends on it. Written
56
+ as `from . cimport base`, the dependency went unseen and stale modules
57
+ warned about a changed `DepthStack` size.
58
+
59
+ ## [0.46.1]
60
+
61
+ ### Changed
62
+ - Each compiled wheel is built and tested in its own CI job, so a
63
+ release takes minutes rather than the better part of an hour. The CLI
64
+ tests write their input files under pytest's `tmp_path`, which
65
+ Windows accepts.
66
+
67
+ ## [0.46.0]
68
+
69
+ ### Added
70
+ - The engine compiles to C. Wheels for CPython 3.11-3.14 on Linux
71
+ (glibc and musl, x86_64 and aarch64), macOS (Apple Silicon) and
72
+ Windows carry the compiled engine; every other Python installs the
73
+ same code as pure Python from the universal wheel. `DOTTED_NATIVE=0`
74
+ in the environment runs the Python engine regardless;
75
+ `dotted.native.active()` says which is loaded. Wildcard reads are
76
+ about 2.9x faster than 0.45.1 compiled and writes about 3.5x; pure
77
+ Python is faster too, writes by about a third.
78
+ - For development: `make test` and `make bench` run both engines,
79
+ `test.python`/`test.native` and `bench.python`/`bench.native` one;
80
+ `make test.wheel` runs the suite against a wheel built from the
81
+ source distribution.
82
+
83
+ ### Changed
84
+ - The `all` extra now means all of them: `formats` and `copium`. It was
85
+ an alias for `formats`, deprecated since 0.44.9.
86
+ - A concrete key present in a node is no longer looked for among the
87
+ node's keys to return the node's own key object: it matches as
88
+ written. Concrete writes and `expand`, `pluck` and `unpack` on a
89
+ concrete key cost the same wherever the key sits in a large dict
90
+ (`update` of the 996th key of 1000: 11.5us to 3.3us compiled). The
91
+ one visible difference: a rendered path shows the key as written
92
+ where the node spells it differently but equal, `users.1` for a key
93
+ `1.0`, `RED` for a `StrEnum` member.
94
+
6
95
  ## [0.45.1]
7
96
 
8
97
  ### Changed
@@ -0,0 +1,4 @@
1
+ include CHANGELOG.md
2
+ recursive-include dotted *.pxd
3
+ recursive-exclude dotted *.c *.so *.pyd
4
+ prune tests
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.45.1
3
+ Version: 0.47.0
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
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/freywaid/dotted
8
8
  Project-URL: Source, https://github.com/freywaid/dotted
9
9
  Project-URL: Changelog, https://github.com/freywaid/dotted/blob/master/CHANGELOG.md
10
10
  Keywords: dotted,nested,path,pattern-matching,json,jsonb,sql,traversal
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: MIT License
14
13
  Classifier: Operating System :: OS Independent
15
14
  Classifier: Programming Language :: Python :: 3
16
15
  Classifier: Programming Language :: Python :: 3.6
@@ -21,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.10
21
20
  Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
23
22
  Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
25
  Classifier: Topic :: Utilities
26
26
  Requires-Python: >=3.6
@@ -39,6 +39,7 @@ Requires-Dist: PyYAML>=5.0; extra == "formats"
39
39
  Requires-Dist: tomli>=1.0; extra == "formats"
40
40
  Requires-Dist: tomli_w>=1.0; extra == "formats"
41
41
  Provides-Extra: all
42
+ Requires-Dist: copium>=0.1.0; extra == "all"
42
43
  Requires-Dist: PyYAML>=5.0; extra == "all"
43
44
  Requires-Dist: tomli>=1.0; extra == "all"
44
45
  Requires-Dist: tomli_w>=1.0; extra == "all"
@@ -73,8 +74,9 @@ Or pick only what you need:
73
74
 
74
75
  pip install dotted-notation[yaml,toml]
75
76
 
76
- > **Deprecated:** the `all` extra is an alias for `formats` and will be removed
77
- > in a future release. Use `formats` instead.
77
+ Or everything, formats and copium (below) alike:
78
+
79
+ pip install dotted-notation[all]
78
80
 
79
81
  For faster `mutable=False` updates and removes, install the
80
82
  [copium](https://github.com/Bobronium/copium) extra, a C implementation of
@@ -142,6 +144,7 @@ For faster `mutable=False` updates and removes, install the
142
144
  - [Template bindings](#template-bindings)
143
145
  - [References](#references)
144
146
  - [Relative References](#relative-references)
147
+ - [References in transforms and guards](#references-in-transforms-and-guards)
145
148
  - [Escaping](#escaping)
146
149
  - [Type Restrictions](#type-restrictions)
147
150
  - [Positive restrictions](#positive-restrictions)
@@ -1372,6 +1375,7 @@ at replace time) and **references** (resolved during traversal).
1372
1375
  | `$$(path)` | Reference | Root object during traversal |
1373
1376
  | `$$(^path)` | Relative reference | Current node during traversal |
1374
1377
  | `$$(^^path)` | Relative reference | Parent node during traversal |
1378
+ | `$$(^path)` in a transform, guard or filter | Relative reference | The value under test; `^^` the node holding it |
1375
1379
 
1376
1380
  <a id="substitution"></a>
1377
1381
  ### Substitution
@@ -1557,6 +1561,50 @@ Relative references combine with patterns:
1557
1561
  >>> dotted.get(data, 'a.$$(^config.*.field)')
1558
1562
  ('Alice', 30)
1559
1563
 
1564
+ <a id="references-in-transforms-and-guards"></a>
1565
+ ### References in transforms and guards
1566
+
1567
+ A reference can also stand in for a transform argument, a guard value or a
1568
+ filter value. It resolves when the transform or comparison runs, against the
1569
+ value under test: `$$(path)` is the root as always, `$$(^path)` is the value
1570
+ itself, `$$(^^path)` the node holding it, `$$(^^^path)` that node's parent,
1571
+ and so on:
1572
+
1573
+ >>> data = {'config': {'offset': 10}, 'n': 5}
1574
+ >>> dotted.get(data, 'n|add:$$(config.offset)')
1575
+ 15
1576
+
1577
+ >>> data = {'items': [{'price': 100, 'rate': 1.5}, {'price': 20, 'rate': 2}]}
1578
+ >>> dotted.get(data, 'items[*].price|mul:$$(^^rate)')
1579
+ (150.0, 40)
1580
+
1581
+ Each `price` is the value being transformed, so `^^rate` is the `rate` beside
1582
+ it. Guard and filter values resolve the same way:
1583
+
1584
+ >>> data = {'limit': 10, 'a': 5, 'b': 15}
1585
+ >>> dotted.get(data, '*>$$(limit)')
1586
+ (15,)
1587
+ >>> data = {'selected': 'bob', 'users': [{'name': 'alice'}, {'name': 'bob'}]}
1588
+ >>> dotted.get(data, 'users[*&name=$$(selected)].name')
1589
+ ('bob',)
1590
+
1591
+ as do the transforms before a guard:
1592
+
1593
+ >>> data = {'scale': 10, 'a': 1, 'b': 2}
1594
+ >>> dotted.get(data, '*|mul:$$(scale)=20')
1595
+ (2,)
1596
+
1597
+ With `update`, the transforms apply to the value being set, so `^` is that
1598
+ value:
1599
+
1600
+ >>> dotted.update({'n': 5, 'inc': 3}, 'n|add:$$(inc)', 10)
1601
+ {'n': 13, 'inc': 3}
1602
+
1603
+ A reference that does not resolve matches nothing, as in an access position:
1604
+
1605
+ >>> dotted.get({'a': 1}, 'a|add:$$(missing)', default='fallback')
1606
+ 'fallback'
1607
+
1560
1608
  <a id="escaping"></a>
1561
1609
  ### Escaping
1562
1610
 
@@ -1584,17 +1632,19 @@ To use a literal `$$` as a key, escape with backslash (`\$$`) or quote it
1584
1632
  <a id="type-restrictions"></a>
1585
1633
  ## Type Restrictions
1586
1634
 
1587
- A **type restriction** constrains a path segment so it only applies when the
1588
- current node is a specific type. Append `:type` after any accessor — key,
1589
- slot, or attr:
1635
+ A **type restriction** keeps a value only if it is of a given type. Append
1636
+ `:type` to a path: `x:int` is `x`, when its value is an `int`, so it reads
1637
+ like a guard, `x=5`, with a type in place of a value:
1590
1638
 
1591
1639
  >>> import dotted
1592
1640
  >>> d = {'a': {'b': 1}, 'c': [1, 2], 'd': 'hello'}
1641
+ >>> dotted.get(d, '*:dict')
1642
+ ({'b': 1},)
1593
1643
  >>> dotted.get(d, '*:dict.*')
1594
1644
  (1,)
1595
1645
 
1596
- Here `*:dict` matches all keys but only descends into values that are dicts.
1597
- The list and string are skipped.
1646
+ Here `*:dict` keeps the values that are dicts, and `.*` continues into them.
1647
+ The list and string are dropped.
1598
1648
 
1599
1649
  Supported types: `str`, `bytes`, `int`, `float`, `dict`, `list`, `tuple`,
1600
1650
  `set`, `frozenset`, `bool`.
@@ -1624,17 +1674,16 @@ Supported types: `str`, `bytes`, `int`, `float`, `dict`, `list`, `tuple`,
1624
1674
 
1625
1675
  Type restrictions work on slot and attr accessors too:
1626
1676
 
1627
- >>> dotted.get([10, 20], '[0]:list')
1628
- 10
1629
- >>> dotted.get((10, 20), '[0]:list', 'missing')
1677
+ >>> dotted.get([10, 'x', 20], '[*]:int')
1678
+ (10, 20)
1679
+ >>> dotted.get([10, 20], '[0]:list', 'missing')
1630
1680
  'missing'
1631
- >>> dotted.get((10, 20), '[0]:tuple')
1632
- 10
1633
1681
 
1634
- Use `:!(str, bytes)` on slot wildcards to prevent string and bytes decomposition:
1682
+ A string or bytes value decomposes under `[*]`; to keep that from happening,
1683
+ restrict the value before the slot:
1635
1684
 
1636
1685
  >>> d = {'s': 'abc', 'b': b'xyz', 'l': [1, 2]}
1637
- >>> dotted.get(d, '*[*]:!(str, bytes)')
1686
+ >>> dotted.get(d, '*:!(str, bytes)[*]')
1638
1687
  (1, 2)
1639
1688
 
1640
1689
  <a id="combining-with-filters"></a>
@@ -1646,11 +1695,23 @@ Type restrictions compose with filters. The restriction goes before the filter:
1646
1695
  >>> dotted.get(d, '*:dict&x=1.*')
1647
1696
  (1, 2)
1648
1697
 
1698
+ <a id="typing-a-transformed-value"></a>
1699
+ ### Typing a transformed value
1700
+
1701
+ A restriction after a parenthesized group applies to what the group yields,
1702
+ transforms included. A `:` directly after a transform name is a parameter,
1703
+ `a|int:16`, so the parentheses are what make it a type:
1704
+
1705
+ >>> dotted.get({'a': '5', 'b': 'x'}, '(*|int):int')
1706
+ (5,)
1707
+
1708
+ Here `|int` leaves `'x'` as it was, and `:int` drops it.
1709
+
1649
1710
  <a id="recursive-traversal-with-type-restrictions"></a>
1650
1711
  ### Recursive traversal with type restrictions
1651
1712
 
1652
- Type restrictions are especially useful in recursive accessor groups to control
1653
- which containers are traversed:
1713
+ Type restrictions are especially useful in recursive accessor groups to keep
1714
+ the traversal to values of certain types:
1654
1715
 
1655
1716
  >>> d = {'a': {'b': [1, 2]}, 'c': 'hello'}
1656
1717
  >>> dotted.get(d, '*(*#, [*]:!(str, bytes))')
@@ -1660,11 +1721,12 @@ Without `:!(str, bytes)`, the `[*]` slot accessor would decompose `'hello'`
1660
1721
  into individual characters.
1661
1722
 
1662
1723
  A type restriction can also be applied to an entire group, which distributes it
1663
- to every accessor in the group:
1724
+ to every accessor in the group. Here it drops the string `'hello'` as well,
1725
+ since the `*` accessor yields it too:
1664
1726
 
1665
1727
  >>> d = {'a': {'b': [1, 2]}, 'c': 'hello'}
1666
1728
  >>> dotted.get(d, '*(*#, [*]):!(str, bytes)')
1667
- ({'b': [1, 2]}, [1, 2], 1, 2, 'hello')
1729
+ ({'b': [1, 2]}, [1, 2], 1, 2)
1668
1730
 
1669
1731
  The `**` shorthand accepts a type restriction too. It can be combined with a
1670
1732
  depth slice — the type restriction comes first:
@@ -2015,27 +2077,45 @@ Updates and removes apply to all non-matching keys:
2015
2077
  <a id="precedence"></a>
2016
2078
  ### Precedence
2017
2079
 
2018
- Access ops (`.`, `@`, `[]`) bind tightest—they build compound paths
2019
- before any operator sees them. Then `!`, then `&`, then `,`:
2080
+ Tightest first:
2081
+
2082
+ | Level | Operators | Example |
2083
+ |-------|-----------|---------|
2084
+ | 1 | access ops `.` `@` `[]`, parentheses | `!a.b` is `!(a.b)` |
2085
+ | 2 | `!` negation | `!a=5` is `(!a)=5` |
2086
+ | 3 | `:type`, `\|transform`, guard `=value`, filter `&key=value` | `!a\|int=5` tests `int` of each `(!a)` value |
2087
+ | 4 | `&` conjunction | `!a=5&b` is `((!a)=5)&b` |
2088
+ | 5 | `,` disjunction | `a,b\|int` is `a,(b\|int)` |
2020
2089
 
2021
- | Precedence | Operator | Example |
2022
- |------------|----------|---------|
2023
- | tightest | `.` `@` `[]` (access ops) | `a.b` is one path |
2024
- | | `!` (negation) | `!a.b` = `!(a.b)` |
2025
- | | `&` (conjunction) | `!a&b` = `(!a)&b` |
2026
- | loosest | `,` (disjunction) | `!a&b,c` = `((!a)&b),c` |
2090
+ A guard, filter or transform applies to the whole expression on its left:
2091
+ an access path (on its last segment), a `!` expression (on what it
2092
+ yields), or a parenthesized group, where it is shorthand for writing it on
2093
+ every branch: `(a,b)=5` is `(a=5,b=5)` and `(a,b)|int` is `(a|int,b|int)`,
2094
+ which is also how `assemble` writes them.
2027
2095
 
2028
2096
  >>> d = {'a': 1, 'b': 2, 'c': 3}
2029
- >>> dotted.get(d, '!a&b,c') # parsed as ((!a)&b), c
2097
+ >>> dotted.get(d, '!a&b,c') # ((!a)&b), c
2030
2098
  (2, 3, 2, 3)
2099
+ >>> dotted.get(d, '!a=3') # (!a)=3: the values other than a's equal to 3
2100
+ (3,)
2101
+ >>> dotted.get(d, '!(a=3)') # everything but an a equal to 3
2102
+ (1, 2, 3)
2103
+ >>> dotted.get({'a': '1', 'b': '2'}, '(a,b)|int')
2104
+ (1, 2)
2105
+ >>> dotted.get({'a': '1', 'b': '2'}, 'a,b|int')
2106
+ ('1', 2)
2031
2107
 
2032
- Since `.` binds tightest, `!a.b&c` is `(!a.b) & c`, not `!(a.b&c)`:
2108
+ Since access ops bind tightest, `!a.b` negates `a` and continues into `b`
2109
+ from every other key, and `!a.b&x` conjoins that with `x`:
2033
2110
 
2034
- >>> d = {'a': {'b': 1}, 'c': 2, 'x': 3}
2035
- >>> dotted.get(d, '!a.b&c') # (!a.b)&c — negate a.b, conjoin with c
2036
- (2, 3, 2)
2037
- >>> dotted.get(d, '!(a.b&c)') # negate the conjunction as a unit
2038
- (3,)
2111
+ >>> d = {'a': {'b': 1}, 'c': {'b': 2}, 'x': 3}
2112
+ >>> dotted.get(d, '!a.b')
2113
+ (2,)
2114
+ >>> dotted.get(d, '!a.b&x')
2115
+ (2, 3)
2116
+
2117
+ A `:` after a transform name is always a parameter, `a|int:16`; to type a
2118
+ transformed value, parenthesize: `(a|int):int`.
2039
2119
 
2040
2120
  <a id="operators"></a>
2041
2121
  ## Operators
@@ -2414,8 +2494,8 @@ Filters support `True`, `False`, and `None` as values:
2414
2494
  ### Value guard
2415
2495
 
2416
2496
  A **value guard** tests the value at a path and yields it only if it matches.
2417
- Use `key=value` or `[slot]=value` after accessing a field. All comparison
2418
- operators (`=`, `!=`, `<`, `>`, `<=`, `>=`) are supported:
2497
+ Use `key=value`, `[slot]=value` or `@attr=value` after accessing a field. All
2498
+ comparison operators (`=`, `!=`, `<`, `>`, `<=`, `>=`) are supported:
2419
2499
 
2420
2500
  >>> d = {'first': 7, 'last': 3}
2421
2501
  >>> dotted.get(d, 'first=7')
@@ -2434,6 +2514,19 @@ For lists of primitive values, use `[*]=value`:
2434
2514
  >>> dotted.get(data, '[0]=1')
2435
2515
  1
2436
2516
 
2517
+ A guard after a parenthesized group is shorthand for a guard on every
2518
+ branch: `(first,last)>5` is `(first>5,last>5)`, and `(first&last)>5` is
2519
+ `(first>5&last>5)`, which yields nothing unless both pass. After `!` it
2520
+ tests what the negation yields, so `!first=3` is the non-`first` values
2521
+ equal to 3; see [Precedence](#precedence).
2522
+
2523
+ >>> dotted.get(d, '(first,last)>5')
2524
+ (7,)
2525
+ >>> dotted.get(d, '(first&last)>5')
2526
+ ()
2527
+ >>> dotted.get(d, '!first=3')
2528
+ (3,)
2529
+
2437
2530
  Guards support all value types: numbers, `None`, `True`/`False`, strings, regex, `*`,
2438
2531
  and [container patterns](#container-filter-values) (`[1, ...]`, `{"a": 1, ...: *}`, `{1, 2, ...}`):
2439
2532
 
@@ -2521,6 +2614,9 @@ Guard transforms compose with `update`, `remove`, and `has`:
2521
2614
  >>> dotted.has({'val': '7'}, 'val|int=7')
2522
2615
  True
2523
2616
 
2617
+ Both the transform arguments and the value compared with may be
2618
+ [references](#references-in-transforms-and-guards).
2619
+
2524
2620
  <a id="container-filter-values"></a>
2525
2621
  ### Container filter values
2526
2622
 
@@ -2836,7 +2932,9 @@ Filter keys can include slice notation so the comparison applies to a slice of t
2836
2932
  You can optionally add transforms to the end of dotted notation. These will
2837
2933
  be applied on `get` and `update`. Transforms are separated by the `|` operator
2838
2934
  and multiple may be chained together. Transforms may be parameterized using
2839
- the `:` operator.
2935
+ the `:` operator; a parameter may be a `$$(reference)`, resolved when the
2936
+ transform runs (see
2937
+ [References in transforms and guards](#references-in-transforms-and-guards)).
2840
2938
 
2841
2939
  >>> import dotted
2842
2940
  >>> d = [1, '2', 3]