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.
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/CHANGELOG.md +53 -0
- {dotted_notation-0.46.1/dotted_notation.egg-info → dotted_notation-0.47.0}/PKG-INFO +131 -35
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/README.md +130 -34
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/access.pxd +3 -3
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/access.py +25 -18
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/api.py +23 -21
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/base.pxd +6 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/base.py +47 -3
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/engine.pxd +3 -3
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/engine.py +13 -11
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/filters.pxd +4 -4
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/filters.py +68 -64
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/grammar.py +140 -183
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/groups.pxd +1 -1
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/groups.py +51 -1
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/matchers.py +8 -6
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/recursive.py +3 -3
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/results.py +93 -59
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/sql/pg.py +14 -5
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/wrappers.pxd +5 -5
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/wrappers.py +255 -75
- {dotted_notation-0.46.1 → dotted_notation-0.47.0/dotted_notation.egg-info}/PKG-INFO +131 -35
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/pyproject.toml +1 -1
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/LICENSE +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/MANIFEST.in +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/__init__.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/__main__.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/cli/main.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/containers.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/matchers.pxd +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/native.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/predicates.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/recursive.pxd +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/sql/core.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/transforms.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/utils.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted/utypes.py +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.46.1 → dotted_notation-0.47.0}/setup.cfg +0 -0
- {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.
|
|
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**
|
|
1590
|
-
|
|
1591
|
-
|
|
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`
|
|
1599
|
-
The list and string are
|
|
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], '[
|
|
1630
|
-
10
|
|
1631
|
-
>>> dotted.get(
|
|
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
|
-
|
|
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, '
|
|
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
|
|
1655
|
-
|
|
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
|
|
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
|
-
|
|
2021
|
-
|
|
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
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
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') #
|
|
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
|
|
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
|
|
2038
|
-
(2,
|
|
2039
|
-
>>> dotted.get(d, '!
|
|
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
|
|
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**
|
|
1543
|
-
|
|
1544
|
-
|
|
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`
|
|
1552
|
-
The list and string are
|
|
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], '[
|
|
1583
|
-
10
|
|
1584
|
-
>>> dotted.get(
|
|
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
|
-
|
|
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, '
|
|
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
|
|
1608
|
-
|
|
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
|
|
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
|
-
|
|
1974
|
-
|
|
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
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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') #
|
|
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
|
|
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
|
|
1991
|
-
(2,
|
|
1992
|
-
>>> dotted.get(d, '!
|
|
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
|
|
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
|
-
|
|
3
|
-
|
|
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):
|