dotted-notation 0.46.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 (48) hide show
  1. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/CHANGELOG.md +53 -0
  2. {dotted_notation-0.46.1/dotted_notation.egg-info → dotted_notation-0.47.0}/PKG-INFO +131 -35
  3. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/README.md +130 -34
  4. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/access.pxd +3 -3
  5. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/access.py +25 -18
  6. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/api.py +23 -21
  7. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/base.pxd +6 -0
  8. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/base.py +47 -3
  9. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/engine.pxd +3 -3
  10. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/engine.py +13 -11
  11. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/filters.pxd +4 -4
  12. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/filters.py +68 -64
  13. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/grammar.py +140 -183
  14. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/groups.pxd +1 -1
  15. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/groups.py +51 -1
  16. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/matchers.py +8 -6
  17. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/recursive.py +3 -3
  18. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/results.py +93 -59
  19. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/sql/pg.py +14 -5
  20. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/wrappers.pxd +5 -5
  21. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/wrappers.py +255 -75
  22. {dotted_notation-0.46.1 → dotted_notation-0.47.0/dotted_notation.egg-info}/PKG-INFO +131 -35
  23. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/pyproject.toml +1 -1
  24. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/LICENSE +0 -0
  25. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/MANIFEST.in +0 -0
  26. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/__init__.py +0 -0
  27. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/__main__.py +0 -0
  28. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/cli/__init__.py +0 -0
  29. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/cli/_compat.py +0 -0
  30. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/cli/formats.py +0 -0
  31. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/cli/main.py +0 -0
  32. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/containers.py +0 -0
  33. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/matchers.pxd +0 -0
  34. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/native.py +0 -0
  35. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/predicates.py +0 -0
  36. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/recursive.pxd +0 -0
  37. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/sql/__init__.py +0 -0
  38. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/sql/core.py +0 -0
  39. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/transforms.py +0 -0
  40. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/utils.py +0 -0
  41. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/utypes.py +0 -0
  42. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/SOURCES.txt +0 -0
  43. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/dependency_links.txt +0 -0
  44. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/entry_points.txt +0 -0
  45. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/requires.txt +0 -0
  46. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/top_level.txt +0 -0
  47. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/setup.cfg +0 -0
  48. {dotted_notation-0.46.1 → dotted_notation-0.47.0}/setup.py +0 -0
@@ -3,6 +3,59 @@
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
+
6
59
  ## [0.46.1]
7
60
 
8
61
  ### 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.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
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)
@@ -1374,6 +1375,7 @@ at replace time) and **references** (resolved during traversal).
1374
1375
  | `$$(path)` | Reference | Root object during traversal |
1375
1376
  | `$$(^path)` | Relative reference | Current node during traversal |
1376
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 |
1377
1379
 
1378
1380
  <a id="substitution"></a>
1379
1381
  ### Substitution
@@ -1559,6 +1561,50 @@ Relative references combine with patterns:
1559
1561
  >>> dotted.get(data, 'a.$$(^config.*.field)')
1560
1562
  ('Alice', 30)
1561
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
+
1562
1608
  <a id="escaping"></a>
1563
1609
  ### Escaping
1564
1610
 
@@ -1586,17 +1632,19 @@ To use a literal `$$` as a key, escape with backslash (`\$$`) or quote it
1586
1632
  <a id="type-restrictions"></a>
1587
1633
  ## Type Restrictions
1588
1634
 
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:
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:
1592
1638
 
1593
1639
  >>> import dotted
1594
1640
  >>> d = {'a': {'b': 1}, 'c': [1, 2], 'd': 'hello'}
1641
+ >>> dotted.get(d, '*:dict')
1642
+ ({'b': 1},)
1595
1643
  >>> dotted.get(d, '*:dict.*')
1596
1644
  (1,)
1597
1645
 
1598
- Here `*:dict` matches all keys but only descends into values that are dicts.
1599
- 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.
1600
1648
 
1601
1649
  Supported types: `str`, `bytes`, `int`, `float`, `dict`, `list`, `tuple`,
1602
1650
  `set`, `frozenset`, `bool`.
@@ -1626,17 +1674,16 @@ Supported types: `str`, `bytes`, `int`, `float`, `dict`, `list`, `tuple`,
1626
1674
 
1627
1675
  Type restrictions work on slot and attr accessors too:
1628
1676
 
1629
- >>> dotted.get([10, 20], '[0]:list')
1630
- 10
1631
- >>> 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')
1632
1680
  'missing'
1633
- >>> dotted.get((10, 20), '[0]:tuple')
1634
- 10
1635
1681
 
1636
- 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:
1637
1684
 
1638
1685
  >>> d = {'s': 'abc', 'b': b'xyz', 'l': [1, 2]}
1639
- >>> dotted.get(d, '*[*]:!(str, bytes)')
1686
+ >>> dotted.get(d, '*:!(str, bytes)[*]')
1640
1687
  (1, 2)
1641
1688
 
1642
1689
  <a id="combining-with-filters"></a>
@@ -1648,11 +1695,23 @@ Type restrictions compose with filters. The restriction goes before the filter:
1648
1695
  >>> dotted.get(d, '*:dict&x=1.*')
1649
1696
  (1, 2)
1650
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
+
1651
1710
  <a id="recursive-traversal-with-type-restrictions"></a>
1652
1711
  ### Recursive traversal with type restrictions
1653
1712
 
1654
- Type restrictions are especially useful in recursive accessor groups to control
1655
- which containers are traversed:
1713
+ Type restrictions are especially useful in recursive accessor groups to keep
1714
+ the traversal to values of certain types:
1656
1715
 
1657
1716
  >>> d = {'a': {'b': [1, 2]}, 'c': 'hello'}
1658
1717
  >>> dotted.get(d, '*(*#, [*]:!(str, bytes))')
@@ -1662,11 +1721,12 @@ Without `:!(str, bytes)`, the `[*]` slot accessor would decompose `'hello'`
1662
1721
  into individual characters.
1663
1722
 
1664
1723
  A type restriction can also be applied to an entire group, which distributes it
1665
- 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:
1666
1726
 
1667
1727
  >>> d = {'a': {'b': [1, 2]}, 'c': 'hello'}
1668
1728
  >>> dotted.get(d, '*(*#, [*]):!(str, bytes)')
1669
- ({'b': [1, 2]}, [1, 2], 1, 2, 'hello')
1729
+ ({'b': [1, 2]}, [1, 2], 1, 2)
1670
1730
 
1671
1731
  The `**` shorthand accepts a type restriction too. It can be combined with a
1672
1732
  depth slice — the type restriction comes first:
@@ -2017,27 +2077,45 @@ Updates and removes apply to all non-matching keys:
2017
2077
  <a id="precedence"></a>
2018
2078
  ### Precedence
2019
2079
 
2020
- Access ops (`.`, `@`, `[]`) bind tightest—they build compound paths
2021
- 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)` |
2022
2089
 
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` |
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.
2029
2095
 
2030
2096
  >>> d = {'a': 1, 'b': 2, 'c': 3}
2031
- >>> dotted.get(d, '!a&b,c') # parsed as ((!a)&b), c
2097
+ >>> dotted.get(d, '!a&b,c') # ((!a)&b), c
2032
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)
2033
2107
 
2034
- 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`:
2035
2110
 
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,)
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`.
2041
2119
 
2042
2120
  <a id="operators"></a>
2043
2121
  ## Operators
@@ -2416,8 +2494,8 @@ Filters support `True`, `False`, and `None` as values:
2416
2494
  ### Value guard
2417
2495
 
2418
2496
  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:
2497
+ Use `key=value`, `[slot]=value` or `@attr=value` after accessing a field. All
2498
+ comparison operators (`=`, `!=`, `<`, `>`, `<=`, `>=`) are supported:
2421
2499
 
2422
2500
  >>> d = {'first': 7, 'last': 3}
2423
2501
  >>> dotted.get(d, 'first=7')
@@ -2436,6 +2514,19 @@ For lists of primitive values, use `[*]=value`:
2436
2514
  >>> dotted.get(data, '[0]=1')
2437
2515
  1
2438
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
+
2439
2530
  Guards support all value types: numbers, `None`, `True`/`False`, strings, regex, `*`,
2440
2531
  and [container patterns](#container-filter-values) (`[1, ...]`, `{"a": 1, ...: *}`, `{1, 2, ...}`):
2441
2532
 
@@ -2523,6 +2614,9 @@ Guard transforms compose with `update`, `remove`, and `has`:
2523
2614
  >>> dotted.has({'val': '7'}, 'val|int=7')
2524
2615
  True
2525
2616
 
2617
+ Both the transform arguments and the value compared with may be
2618
+ [references](#references-in-transforms-and-guards).
2619
+
2526
2620
  <a id="container-filter-values"></a>
2527
2621
  ### Container filter values
2528
2622
 
@@ -2838,7 +2932,9 @@ Filter keys can include slice notation so the comparison applies to a slice of t
2838
2932
  You can optionally add transforms to the end of dotted notation. These will
2839
2933
  be applied on `get` and `update`. Transforms are separated by the `|` operator
2840
2934
  and multiple may be chained together. Transforms may be parameterized using
2841
- 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)).
2842
2938
 
2843
2939
  >>> import dotted
2844
2940
  >>> d = [1, '2', 3]
@@ -97,6 +97,7 @@ For faster `mutable=False` updates and removes, install the
97
97
  - [Template bindings](#template-bindings)
98
98
  - [References](#references)
99
99
  - [Relative References](#relative-references)
100
+ - [References in transforms and guards](#references-in-transforms-and-guards)
100
101
  - [Escaping](#escaping)
101
102
  - [Type Restrictions](#type-restrictions)
102
103
  - [Positive restrictions](#positive-restrictions)
@@ -1327,6 +1328,7 @@ at replace time) and **references** (resolved during traversal).
1327
1328
  | `$$(path)` | Reference | Root object during traversal |
1328
1329
  | `$$(^path)` | Relative reference | Current node during traversal |
1329
1330
  | `$$(^^path)` | Relative reference | Parent node during traversal |
1331
+ | `$$(^path)` in a transform, guard or filter | Relative reference | The value under test; `^^` the node holding it |
1330
1332
 
1331
1333
  <a id="substitution"></a>
1332
1334
  ### Substitution
@@ -1512,6 +1514,50 @@ Relative references combine with patterns:
1512
1514
  >>> dotted.get(data, 'a.$$(^config.*.field)')
1513
1515
  ('Alice', 30)
1514
1516
 
1517
+ <a id="references-in-transforms-and-guards"></a>
1518
+ ### References in transforms and guards
1519
+
1520
+ A reference can also stand in for a transform argument, a guard value or a
1521
+ filter value. It resolves when the transform or comparison runs, against the
1522
+ value under test: `$$(path)` is the root as always, `$$(^path)` is the value
1523
+ itself, `$$(^^path)` the node holding it, `$$(^^^path)` that node's parent,
1524
+ and so on:
1525
+
1526
+ >>> data = {'config': {'offset': 10}, 'n': 5}
1527
+ >>> dotted.get(data, 'n|add:$$(config.offset)')
1528
+ 15
1529
+
1530
+ >>> data = {'items': [{'price': 100, 'rate': 1.5}, {'price': 20, 'rate': 2}]}
1531
+ >>> dotted.get(data, 'items[*].price|mul:$$(^^rate)')
1532
+ (150.0, 40)
1533
+
1534
+ Each `price` is the value being transformed, so `^^rate` is the `rate` beside
1535
+ it. Guard and filter values resolve the same way:
1536
+
1537
+ >>> data = {'limit': 10, 'a': 5, 'b': 15}
1538
+ >>> dotted.get(data, '*>$$(limit)')
1539
+ (15,)
1540
+ >>> data = {'selected': 'bob', 'users': [{'name': 'alice'}, {'name': 'bob'}]}
1541
+ >>> dotted.get(data, 'users[*&name=$$(selected)].name')
1542
+ ('bob',)
1543
+
1544
+ as do the transforms before a guard:
1545
+
1546
+ >>> data = {'scale': 10, 'a': 1, 'b': 2}
1547
+ >>> dotted.get(data, '*|mul:$$(scale)=20')
1548
+ (2,)
1549
+
1550
+ With `update`, the transforms apply to the value being set, so `^` is that
1551
+ value:
1552
+
1553
+ >>> dotted.update({'n': 5, 'inc': 3}, 'n|add:$$(inc)', 10)
1554
+ {'n': 13, 'inc': 3}
1555
+
1556
+ A reference that does not resolve matches nothing, as in an access position:
1557
+
1558
+ >>> dotted.get({'a': 1}, 'a|add:$$(missing)', default='fallback')
1559
+ 'fallback'
1560
+
1515
1561
  <a id="escaping"></a>
1516
1562
  ### Escaping
1517
1563
 
@@ -1539,17 +1585,19 @@ To use a literal `$$` as a key, escape with backslash (`\$$`) or quote it
1539
1585
  <a id="type-restrictions"></a>
1540
1586
  ## Type Restrictions
1541
1587
 
1542
- A **type restriction** constrains a path segment so it only applies when the
1543
- current node is a specific type. Append `:type` after any accessor — key,
1544
- slot, or attr:
1588
+ A **type restriction** keeps a value only if it is of a given type. Append
1589
+ `:type` to a path: `x:int` is `x`, when its value is an `int`, so it reads
1590
+ like a guard, `x=5`, with a type in place of a value:
1545
1591
 
1546
1592
  >>> import dotted
1547
1593
  >>> d = {'a': {'b': 1}, 'c': [1, 2], 'd': 'hello'}
1594
+ >>> dotted.get(d, '*:dict')
1595
+ ({'b': 1},)
1548
1596
  >>> dotted.get(d, '*:dict.*')
1549
1597
  (1,)
1550
1598
 
1551
- Here `*:dict` matches all keys but only descends into values that are dicts.
1552
- The list and string are skipped.
1599
+ Here `*:dict` keeps the values that are dicts, and `.*` continues into them.
1600
+ The list and string are dropped.
1553
1601
 
1554
1602
  Supported types: `str`, `bytes`, `int`, `float`, `dict`, `list`, `tuple`,
1555
1603
  `set`, `frozenset`, `bool`.
@@ -1579,17 +1627,16 @@ Supported types: `str`, `bytes`, `int`, `float`, `dict`, `list`, `tuple`,
1579
1627
 
1580
1628
  Type restrictions work on slot and attr accessors too:
1581
1629
 
1582
- >>> dotted.get([10, 20], '[0]:list')
1583
- 10
1584
- >>> dotted.get((10, 20), '[0]:list', 'missing')
1630
+ >>> dotted.get([10, 'x', 20], '[*]:int')
1631
+ (10, 20)
1632
+ >>> dotted.get([10, 20], '[0]:list', 'missing')
1585
1633
  'missing'
1586
- >>> dotted.get((10, 20), '[0]:tuple')
1587
- 10
1588
1634
 
1589
- Use `:!(str, bytes)` on slot wildcards to prevent string and bytes decomposition:
1635
+ A string or bytes value decomposes under `[*]`; to keep that from happening,
1636
+ restrict the value before the slot:
1590
1637
 
1591
1638
  >>> d = {'s': 'abc', 'b': b'xyz', 'l': [1, 2]}
1592
- >>> dotted.get(d, '*[*]:!(str, bytes)')
1639
+ >>> dotted.get(d, '*:!(str, bytes)[*]')
1593
1640
  (1, 2)
1594
1641
 
1595
1642
  <a id="combining-with-filters"></a>
@@ -1601,11 +1648,23 @@ Type restrictions compose with filters. The restriction goes before the filter:
1601
1648
  >>> dotted.get(d, '*:dict&x=1.*')
1602
1649
  (1, 2)
1603
1650
 
1651
+ <a id="typing-a-transformed-value"></a>
1652
+ ### Typing a transformed value
1653
+
1654
+ A restriction after a parenthesized group applies to what the group yields,
1655
+ transforms included. A `:` directly after a transform name is a parameter,
1656
+ `a|int:16`, so the parentheses are what make it a type:
1657
+
1658
+ >>> dotted.get({'a': '5', 'b': 'x'}, '(*|int):int')
1659
+ (5,)
1660
+
1661
+ Here `|int` leaves `'x'` as it was, and `:int` drops it.
1662
+
1604
1663
  <a id="recursive-traversal-with-type-restrictions"></a>
1605
1664
  ### Recursive traversal with type restrictions
1606
1665
 
1607
- Type restrictions are especially useful in recursive accessor groups to control
1608
- which containers are traversed:
1666
+ Type restrictions are especially useful in recursive accessor groups to keep
1667
+ the traversal to values of certain types:
1609
1668
 
1610
1669
  >>> d = {'a': {'b': [1, 2]}, 'c': 'hello'}
1611
1670
  >>> dotted.get(d, '*(*#, [*]:!(str, bytes))')
@@ -1615,11 +1674,12 @@ Without `:!(str, bytes)`, the `[*]` slot accessor would decompose `'hello'`
1615
1674
  into individual characters.
1616
1675
 
1617
1676
  A type restriction can also be applied to an entire group, which distributes it
1618
- to every accessor in the group:
1677
+ to every accessor in the group. Here it drops the string `'hello'` as well,
1678
+ since the `*` accessor yields it too:
1619
1679
 
1620
1680
  >>> d = {'a': {'b': [1, 2]}, 'c': 'hello'}
1621
1681
  >>> dotted.get(d, '*(*#, [*]):!(str, bytes)')
1622
- ({'b': [1, 2]}, [1, 2], 1, 2, 'hello')
1682
+ ({'b': [1, 2]}, [1, 2], 1, 2)
1623
1683
 
1624
1684
  The `**` shorthand accepts a type restriction too. It can be combined with a
1625
1685
  depth slice — the type restriction comes first:
@@ -1970,27 +2030,45 @@ Updates and removes apply to all non-matching keys:
1970
2030
  <a id="precedence"></a>
1971
2031
  ### Precedence
1972
2032
 
1973
- Access ops (`.`, `@`, `[]`) bind tightest—they build compound paths
1974
- before any operator sees them. Then `!`, then `&`, then `,`:
2033
+ Tightest first:
2034
+
2035
+ | Level | Operators | Example |
2036
+ |-------|-----------|---------|
2037
+ | 1 | access ops `.` `@` `[]`, parentheses | `!a.b` is `!(a.b)` |
2038
+ | 2 | `!` negation | `!a=5` is `(!a)=5` |
2039
+ | 3 | `:type`, `\|transform`, guard `=value`, filter `&key=value` | `!a\|int=5` tests `int` of each `(!a)` value |
2040
+ | 4 | `&` conjunction | `!a=5&b` is `((!a)=5)&b` |
2041
+ | 5 | `,` disjunction | `a,b\|int` is `a,(b\|int)` |
1975
2042
 
1976
- | Precedence | Operator | Example |
1977
- |------------|----------|---------|
1978
- | tightest | `.` `@` `[]` (access ops) | `a.b` is one path |
1979
- | | `!` (negation) | `!a.b` = `!(a.b)` |
1980
- | | `&` (conjunction) | `!a&b` = `(!a)&b` |
1981
- | loosest | `,` (disjunction) | `!a&b,c` = `((!a)&b),c` |
2043
+ A guard, filter or transform applies to the whole expression on its left:
2044
+ an access path (on its last segment), a `!` expression (on what it
2045
+ yields), or a parenthesized group, where it is shorthand for writing it on
2046
+ every branch: `(a,b)=5` is `(a=5,b=5)` and `(a,b)|int` is `(a|int,b|int)`,
2047
+ which is also how `assemble` writes them.
1982
2048
 
1983
2049
  >>> d = {'a': 1, 'b': 2, 'c': 3}
1984
- >>> dotted.get(d, '!a&b,c') # parsed as ((!a)&b), c
2050
+ >>> dotted.get(d, '!a&b,c') # ((!a)&b), c
1985
2051
  (2, 3, 2, 3)
2052
+ >>> dotted.get(d, '!a=3') # (!a)=3: the values other than a's equal to 3
2053
+ (3,)
2054
+ >>> dotted.get(d, '!(a=3)') # everything but an a equal to 3
2055
+ (1, 2, 3)
2056
+ >>> dotted.get({'a': '1', 'b': '2'}, '(a,b)|int')
2057
+ (1, 2)
2058
+ >>> dotted.get({'a': '1', 'b': '2'}, 'a,b|int')
2059
+ ('1', 2)
1986
2060
 
1987
- Since `.` binds tightest, `!a.b&c` is `(!a.b) & c`, not `!(a.b&c)`:
2061
+ Since access ops bind tightest, `!a.b` negates `a` and continues into `b`
2062
+ from every other key, and `!a.b&x` conjoins that with `x`:
1988
2063
 
1989
- >>> d = {'a': {'b': 1}, 'c': 2, 'x': 3}
1990
- >>> dotted.get(d, '!a.b&c') # (!a.b)&c — negate a.b, conjoin with c
1991
- (2, 3, 2)
1992
- >>> dotted.get(d, '!(a.b&c)') # negate the conjunction as a unit
1993
- (3,)
2064
+ >>> d = {'a': {'b': 1}, 'c': {'b': 2}, 'x': 3}
2065
+ >>> dotted.get(d, '!a.b')
2066
+ (2,)
2067
+ >>> dotted.get(d, '!a.b&x')
2068
+ (2, 3)
2069
+
2070
+ A `:` after a transform name is always a parameter, `a|int:16`; to type a
2071
+ transformed value, parenthesize: `(a|int):int`.
1994
2072
 
1995
2073
  <a id="operators"></a>
1996
2074
  ## Operators
@@ -2369,8 +2447,8 @@ Filters support `True`, `False`, and `None` as values:
2369
2447
  ### Value guard
2370
2448
 
2371
2449
  A **value guard** tests the value at a path and yields it only if it matches.
2372
- Use `key=value` or `[slot]=value` after accessing a field. All comparison
2373
- operators (`=`, `!=`, `<`, `>`, `<=`, `>=`) are supported:
2450
+ Use `key=value`, `[slot]=value` or `@attr=value` after accessing a field. All
2451
+ comparison operators (`=`, `!=`, `<`, `>`, `<=`, `>=`) are supported:
2374
2452
 
2375
2453
  >>> d = {'first': 7, 'last': 3}
2376
2454
  >>> dotted.get(d, 'first=7')
@@ -2389,6 +2467,19 @@ For lists of primitive values, use `[*]=value`:
2389
2467
  >>> dotted.get(data, '[0]=1')
2390
2468
  1
2391
2469
 
2470
+ A guard after a parenthesized group is shorthand for a guard on every
2471
+ branch: `(first,last)>5` is `(first>5,last>5)`, and `(first&last)>5` is
2472
+ `(first>5&last>5)`, which yields nothing unless both pass. After `!` it
2473
+ tests what the negation yields, so `!first=3` is the non-`first` values
2474
+ equal to 3; see [Precedence](#precedence).
2475
+
2476
+ >>> dotted.get(d, '(first,last)>5')
2477
+ (7,)
2478
+ >>> dotted.get(d, '(first&last)>5')
2479
+ ()
2480
+ >>> dotted.get(d, '!first=3')
2481
+ (3,)
2482
+
2392
2483
  Guards support all value types: numbers, `None`, `True`/`False`, strings, regex, `*`,
2393
2484
  and [container patterns](#container-filter-values) (`[1, ...]`, `{"a": 1, ...: *}`, `{1, 2, ...}`):
2394
2485
 
@@ -2476,6 +2567,9 @@ Guard transforms compose with `update`, `remove`, and `has`:
2476
2567
  >>> dotted.has({'val': '7'}, 'val|int=7')
2477
2568
  True
2478
2569
 
2570
+ Both the transform arguments and the value compared with may be
2571
+ [references](#references-in-transforms-and-guards).
2572
+
2479
2573
  <a id="container-filter-values"></a>
2480
2574
  ### Container filter values
2481
2575
 
@@ -2791,7 +2885,9 @@ Filter keys can include slice notation so the comparison applies to a slice of t
2791
2885
  You can optionally add transforms to the end of dotted notation. These will
2792
2886
  be applied on `get` and `update`. Transforms are separated by the `|` operator
2793
2887
  and multiple may be chained together. Transforms may be parameterized using
2794
- the `:` operator.
2888
+ the `:` operator; a parameter may be a `$$(reference)`, resolved when the
2889
+ transform runs (see
2890
+ [References in transforms and guards](#references-in-transforms-and-guards)).
2795
2891
 
2796
2892
  >>> import dotted
2797
2893
  >>> d = [1, '2', 3]
@@ -1,12 +1,12 @@
1
1
  # Type declarations for the compiler; see base.pxd.
2
- from . cimport base
3
- from . cimport matchers
2
+ cimport dotted.base as base
3
+ cimport dotted.matchers as matchers
4
4
 
5
5
 
6
6
  cdef class BaseOp(base.TraversalOp):
7
7
  cdef public tuple filters
8
8
 
9
- cpdef filtered(self, items)
9
+ cpdef filtered(self, items, settings=*)
10
10
 
11
11
 
12
12
  cdef class SimpleOp(BaseOp):