dotted-notation 0.49.2__tar.gz → 0.49.4__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.49.2 → dotted_notation-0.49.4}/CHANGELOG.md +48 -0
- {dotted_notation-0.49.2/dotted_notation.egg-info → dotted_notation-0.49.4}/PKG-INFO +27 -1
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/README.md +25 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/access.py +68 -2
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/containers.py +159 -11
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/filters.py +34 -8
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/grammar.py +10 -4
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/recursive.py +40 -8
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/results.py +9 -2
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/wrappers.py +6 -4
- {dotted_notation-0.49.2 → dotted_notation-0.49.4/dotted_notation.egg-info}/PKG-INFO +27 -1
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/pyproject.toml +3 -2
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/setup.py +4 -1
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/LICENSE +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/MANIFEST.in +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/__init__.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/__main__.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/access.pxd +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/api.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/base.pxd +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/base.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/cli/main.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/engine.pxd +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/engine.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/filters.pxd +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/groups.pxd +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/groups.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/matchers.pxd +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/matchers.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/native.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/predicates.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/recursive.pxd +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/sql/core.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/sql/pg.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/transforms.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/utils.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/utypes.py +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted/wrappers.pxd +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.49.2 → dotted_notation-0.49.4}/setup.cfg +0 -0
|
@@ -3,6 +3,54 @@
|
|
|
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.49.4]
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- `update` through `~` around a filtered or recursive op,
|
|
10
|
+
`[(~*&k=v#, +)]` or `~*a.x`, raised `TypeError` once something
|
|
11
|
+
matched (#65). It now keeps what the `~` matches as it is and, as
|
|
12
|
+
elsewhere, still updates the path after it: `[(~*&k=1#, +)].x`
|
|
13
|
+
updates `x` in the matching items.
|
|
14
|
+
|
|
15
|
+
## [0.49.3]
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- A slice bound may be a reference or a substitution: `b[:$$(i)]`,
|
|
19
|
+
`b[$(n):]`, `b[$0:$1]`, `b[::$$(step)]`. It resolves as a slot's does,
|
|
20
|
+
so `b[:$$(i)]|sum` is a running total with `i` bound per element
|
|
21
|
+
(#64). A reference that does not resolve matches nothing.
|
|
22
|
+
- A reference may hold parentheses of its own: `$$()` runs to the `)`
|
|
23
|
+
that closes it, so a group or a pack works inside, `$$(a.(b,c))`,
|
|
24
|
+
`$$((a[*];))`. A `)` inside quotes does not count.
|
|
25
|
+
- A reference inside a container value, `=[1, $$(x)]`, `={"k": $$(x)}`,
|
|
26
|
+
`="ab"...$$(x)`, `=(1, $$(x))`, resolves as a whole guard or filter
|
|
27
|
+
value does. One whose path is a pattern branches the container, once
|
|
28
|
+
per value; pack it, `$$((x[*];))`, to compare with the tuple instead.
|
|
29
|
+
In a string or bytes glob it must resolve to `str` or `bytes`, else
|
|
30
|
+
`TypeError`; `$$(x|str)` coerces.
|
|
31
|
+
- A reference in a filter key part, `[*&$$(field)="bob"]`,
|
|
32
|
+
`[*&tags[$$(^^i)]="b"]`, `[*&xs[:$$(^^n)]=[1]]`, resolves against the
|
|
33
|
+
node that part is looked up in, as `^`; in the first part that is the
|
|
34
|
+
item. A pattern reference tries each key it resolves to.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- An unresolved substitution inside a container value, `=[1, $(x)]`,
|
|
38
|
+
makes the path an unresolved template, so `get` and the rest raise
|
|
39
|
+
`TypeError` as for any other unresolved `$N`, rather than matching
|
|
40
|
+
nothing.
|
|
41
|
+
- Resolving a recursive op's substitutions, `**&x=$(v)` or
|
|
42
|
+
`*($(k), [*])&x=1`, now resolves those in its filters, keeps the
|
|
43
|
+
filters, and keeps `*?`/`**?` first-match. `is_template` and
|
|
44
|
+
`is_reference` see a recursive op's accessors and filters, so an
|
|
45
|
+
unresolved `$N` there raises and a `$$(^^…)` gets its parents.
|
|
46
|
+
- Resolving `$N` in a string or bytes glob no longer turns a `$$(path)`
|
|
47
|
+
part into the literal text of its path.
|
|
48
|
+
- A slice's repr shows its step, `[1:5:2]`, rather than repeating its
|
|
49
|
+
stop.
|
|
50
|
+
- `make native` on macOS, and on Linux from Python 3.14: `setup.py`
|
|
51
|
+
runs `setup()` under a `__main__` guard, since Cython's spawned
|
|
52
|
+
workers import it again.
|
|
53
|
+
|
|
6
54
|
## [0.49.2]
|
|
7
55
|
|
|
8
56
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.49.
|
|
3
|
+
Version: 0.49.4
|
|
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
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
24
25
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
26
|
Classifier: Topic :: Utilities
|
|
26
27
|
Requires-Python: >=3.6
|
|
@@ -1163,6 +1164,14 @@ Dotted slicing works like python slicing and all that entails.
|
|
|
1163
1164
|
>>> dotted.get(d, '*.there[1:]')
|
|
1164
1165
|
([2, 3], [5, 6])
|
|
1165
1166
|
|
|
1167
|
+
A bound may be a reference or a substitution, resolved the way a slot's is:
|
|
1168
|
+
|
|
1169
|
+
>>> d = {'b': [3, 0, 5], 'i': 2}
|
|
1170
|
+
>>> dotted.get(d, 'b[:$$(i)]')
|
|
1171
|
+
[3, 0]
|
|
1172
|
+
>>> dotted.get(d, 'b[$(n):]', bindings={'n': 1})
|
|
1173
|
+
[0, 5]
|
|
1174
|
+
|
|
1166
1175
|
<a id="dot-notation-for-sequence-indexing"></a>
|
|
1167
1176
|
### Dot notation for sequence indexing
|
|
1168
1177
|
|
|
@@ -1641,6 +1650,23 @@ as do the transforms before a guard:
|
|
|
1641
1650
|
>>> dotted.get(data, '*|mul:$$(scale)=20')
|
|
1642
1651
|
(2,)
|
|
1643
1652
|
|
|
1653
|
+
So does a reference inside a container value. One whose path is a pattern
|
|
1654
|
+
branches the container, once per value; pack it, `(…;)`, to compare with the
|
|
1655
|
+
tuple instead:
|
|
1656
|
+
|
|
1657
|
+
>>> data = {'want': [2, 3], 'items': [[1, 2], [1, 4], [1, (2, 3)]]}
|
|
1658
|
+
>>> dotted.get(data, 'items[*]=[1, $$(want[*])]')
|
|
1659
|
+
([1, 2],)
|
|
1660
|
+
>>> dotted.get(data, 'items[*]=[1, $$((want[*];))]')
|
|
1661
|
+
([1, (2, 3)],)
|
|
1662
|
+
|
|
1663
|
+
A filter key may hold references too. In each part of the key, `^` is the
|
|
1664
|
+
node that part is looked up in, so in the first part it is the item:
|
|
1665
|
+
|
|
1666
|
+
>>> data = {'field': 'name', 'users': [{'name': 'alice'}, {'name': 'bob'}]}
|
|
1667
|
+
>>> dotted.get(data, 'users[*&$$(field)="bob"]')
|
|
1668
|
+
({'name': 'bob'},)
|
|
1669
|
+
|
|
1644
1670
|
With `update`, the transforms apply to the value being set, so `^` is that
|
|
1645
1671
|
value:
|
|
1646
1672
|
|
|
@@ -1116,6 +1116,14 @@ Dotted slicing works like python slicing and all that entails.
|
|
|
1116
1116
|
>>> dotted.get(d, '*.there[1:]')
|
|
1117
1117
|
([2, 3], [5, 6])
|
|
1118
1118
|
|
|
1119
|
+
A bound may be a reference or a substitution, resolved the way a slot's is:
|
|
1120
|
+
|
|
1121
|
+
>>> d = {'b': [3, 0, 5], 'i': 2}
|
|
1122
|
+
>>> dotted.get(d, 'b[:$$(i)]')
|
|
1123
|
+
[3, 0]
|
|
1124
|
+
>>> dotted.get(d, 'b[$(n):]', bindings={'n': 1})
|
|
1125
|
+
[0, 5]
|
|
1126
|
+
|
|
1119
1127
|
<a id="dot-notation-for-sequence-indexing"></a>
|
|
1120
1128
|
### Dot notation for sequence indexing
|
|
1121
1129
|
|
|
@@ -1594,6 +1602,23 @@ as do the transforms before a guard:
|
|
|
1594
1602
|
>>> dotted.get(data, '*|mul:$$(scale)=20')
|
|
1595
1603
|
(2,)
|
|
1596
1604
|
|
|
1605
|
+
So does a reference inside a container value. One whose path is a pattern
|
|
1606
|
+
branches the container, once per value; pack it, `(…;)`, to compare with the
|
|
1607
|
+
tuple instead:
|
|
1608
|
+
|
|
1609
|
+
>>> data = {'want': [2, 3], 'items': [[1, 2], [1, 4], [1, (2, 3)]]}
|
|
1610
|
+
>>> dotted.get(data, 'items[*]=[1, $$(want[*])]')
|
|
1611
|
+
([1, 2],)
|
|
1612
|
+
>>> dotted.get(data, 'items[*]=[1, $$((want[*];))]')
|
|
1613
|
+
([1, (2, 3)],)
|
|
1614
|
+
|
|
1615
|
+
A filter key may hold references too. In each part of the key, `^` is the
|
|
1616
|
+
node that part is looked up in, so in the first part it is the item:
|
|
1617
|
+
|
|
1618
|
+
>>> data = {'field': 'name', 'users': [{'name': 'alice'}, {'name': 'bob'}]}
|
|
1619
|
+
>>> dotted.get(data, 'users[*&$$(field)="bob"]')
|
|
1620
|
+
({'name': 'bob'},)
|
|
1621
|
+
|
|
1597
1622
|
With `update`, the transforms apply to the value being set, so `^` is that
|
|
1598
1623
|
value:
|
|
1599
1624
|
|
|
@@ -994,12 +994,49 @@ class Slice(SimpleOp):
|
|
|
994
994
|
start, stop, step = self.args
|
|
995
995
|
m = [s(start), s(stop)]
|
|
996
996
|
if step is not None:
|
|
997
|
-
m += [s(
|
|
997
|
+
m += [s(step)]
|
|
998
998
|
return '[' + ':'.join(m) + ']'
|
|
999
999
|
def is_pattern(self):
|
|
1000
1000
|
return False
|
|
1001
1001
|
def is_slice(self):
|
|
1002
1002
|
return True
|
|
1003
|
+
def is_template(self):
|
|
1004
|
+
"""
|
|
1005
|
+
True if a bound is an unresolved substitution.
|
|
1006
|
+
"""
|
|
1007
|
+
return any(isinstance(a, matchers.Subst) for a in self.args)
|
|
1008
|
+
def is_reference(self):
|
|
1009
|
+
"""
|
|
1010
|
+
True if a bound is an internal reference.
|
|
1011
|
+
"""
|
|
1012
|
+
return any(isinstance(a, matchers.Reference) for a in self.args)
|
|
1013
|
+
def resolve(self, bindings, partial=False):
|
|
1014
|
+
"""
|
|
1015
|
+
Resolve $N in the bounds.
|
|
1016
|
+
"""
|
|
1017
|
+
if not self.is_template():
|
|
1018
|
+
return self
|
|
1019
|
+
args = []
|
|
1020
|
+
for a in self.args:
|
|
1021
|
+
r = a.resolve(bindings, partial) if isinstance(a, matchers.Subst) else a
|
|
1022
|
+
# a resolved substitution is a ResolvedValue; the bound is its value
|
|
1023
|
+
args.append(r.value if isinstance(r, matchers.ResolvedValue) else r)
|
|
1024
|
+
return self.concrete(slice(*args))
|
|
1025
|
+
def _resolved(self, node=None, settings=base.SETTINGS):
|
|
1026
|
+
"""
|
|
1027
|
+
This slice with its $$(path) bounds resolved, or None if one does
|
|
1028
|
+
not resolve.
|
|
1029
|
+
"""
|
|
1030
|
+
args = []
|
|
1031
|
+
for a in self.args:
|
|
1032
|
+
if not isinstance(a, matchers.Reference):
|
|
1033
|
+
args.append(a)
|
|
1034
|
+
continue
|
|
1035
|
+
try:
|
|
1036
|
+
args.append(settings.resolve(a, node, settings.parents or ()))
|
|
1037
|
+
except KeyError:
|
|
1038
|
+
return None
|
|
1039
|
+
return self.concrete(slice(*args))
|
|
1003
1040
|
def operator(self, top=False):
|
|
1004
1041
|
return str(self)
|
|
1005
1042
|
def slice(self, node=None):
|
|
@@ -1012,6 +1049,8 @@ class Slice(SimpleOp):
|
|
|
1012
1049
|
Calculate cardinality of a slice; don't both dealing with countably infinite
|
|
1013
1050
|
set arithmetic, instead just pick a suitably large integer
|
|
1014
1051
|
"""
|
|
1052
|
+
if self.is_template() or self.is_reference():
|
|
1053
|
+
return 0
|
|
1015
1054
|
s = self.slice(node)
|
|
1016
1055
|
start = s.start or 0
|
|
1017
1056
|
stop = s.stop or '+'
|
|
@@ -1023,7 +1062,12 @@ class Slice(SimpleOp):
|
|
|
1023
1062
|
return max(0, int((stop - start) / step))
|
|
1024
1063
|
|
|
1025
1064
|
def keys(self, node, settings=base.SETTINGS):
|
|
1026
|
-
|
|
1065
|
+
if not self.is_reference():
|
|
1066
|
+
return (self.slice(node),)
|
|
1067
|
+
r = self._resolved(node=node, settings=settings)
|
|
1068
|
+
if r is None:
|
|
1069
|
+
return ()
|
|
1070
|
+
return r.keys(node, settings=settings)
|
|
1027
1071
|
def items(self, node, settings=base.SETTINGS, filtered=True):
|
|
1028
1072
|
return self._slice_items(node, settings)
|
|
1029
1073
|
def _slice_items(self, node, settings):
|
|
@@ -1076,6 +1120,28 @@ class Slice(SimpleOp):
|
|
|
1076
1120
|
def upsert(self, node, val):
|
|
1077
1121
|
return self.update(node, self.slice(node), val)
|
|
1078
1122
|
|
|
1123
|
+
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, settings=base.SETTINGS):
|
|
1124
|
+
"""
|
|
1125
|
+
Resolve references before updating.
|
|
1126
|
+
"""
|
|
1127
|
+
if not self.is_reference():
|
|
1128
|
+
return super().do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap, settings=settings)
|
|
1129
|
+
r = self._resolved(node=node, settings=settings)
|
|
1130
|
+
if r is None:
|
|
1131
|
+
return node
|
|
1132
|
+
return r.do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap, settings=settings)
|
|
1133
|
+
|
|
1134
|
+
def do_remove(self, ops, node, val, nop, settings=base.SETTINGS):
|
|
1135
|
+
"""
|
|
1136
|
+
Resolve references before removing.
|
|
1137
|
+
"""
|
|
1138
|
+
if not self.is_reference():
|
|
1139
|
+
return super().do_remove(ops, node, val, nop, settings=settings)
|
|
1140
|
+
r = self._resolved(node=node, settings=settings)
|
|
1141
|
+
if r is None:
|
|
1142
|
+
return node
|
|
1143
|
+
return r.do_remove(ops, node, val, nop, settings=settings)
|
|
1144
|
+
|
|
1079
1145
|
def pop(self, node, key):
|
|
1080
1146
|
try:
|
|
1081
1147
|
del node[key]
|
|
@@ -25,7 +25,7 @@ Type prefix semantics:
|
|
|
25
25
|
fs{...} frozenset only
|
|
26
26
|
"""
|
|
27
27
|
import re
|
|
28
|
-
from itertools import combinations
|
|
28
|
+
from itertools import combinations, product
|
|
29
29
|
|
|
30
30
|
from . import utils
|
|
31
31
|
from . import base
|
|
@@ -46,11 +46,91 @@ def _element_matches(pattern, val):
|
|
|
46
46
|
return any(True for _ in pattern.matches((val,)))
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def _holds(items, method):
|
|
50
|
+
"""
|
|
51
|
+
True if an item, or a part of a (key, value) dict entry, answers True
|
|
52
|
+
to method: 'is_template' or 'is_reference'.
|
|
53
|
+
"""
|
|
54
|
+
for x in items:
|
|
55
|
+
if isinstance(x, tuple):
|
|
56
|
+
if _holds(x, method):
|
|
57
|
+
return True
|
|
58
|
+
continue
|
|
59
|
+
test = getattr(x, method, None)
|
|
60
|
+
if test is not None and test():
|
|
61
|
+
return True
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _values(ref, settings, node, parents):
|
|
66
|
+
"""
|
|
67
|
+
What reference ref resolves to, as a tuple: its value, or each value
|
|
68
|
+
of a pattern reference.
|
|
69
|
+
"""
|
|
70
|
+
val = settings.resolve(ref, node, parents)
|
|
71
|
+
if ref.is_pattern():
|
|
72
|
+
return tuple(val)
|
|
73
|
+
return (val,)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _alternatives(e, settings, node, parents):
|
|
77
|
+
"""
|
|
78
|
+
The forms element e takes once its references resolve: a Const per
|
|
79
|
+
value of a reference, a nested container's branches, each pairing of
|
|
80
|
+
a dict entry's key and value forms; just e when it holds none.
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(e, tuple):
|
|
83
|
+
return tuple(product(*(_alternatives(p, settings, node, parents) for p in e)))
|
|
84
|
+
if not _holds((e,), 'is_reference'):
|
|
85
|
+
return (e,)
|
|
86
|
+
if isinstance(e, matchers.Reference):
|
|
87
|
+
return tuple(matchers.Const(v) for v in _values(e, settings, node, parents))
|
|
88
|
+
return e.branches(settings, node, parents)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _fragments(part, typ, owner, settings, node, parents):
|
|
92
|
+
"""
|
|
93
|
+
The fragments a string or bytes glob part stands for: a reference's
|
|
94
|
+
values, each of which must be a typ; any other part as it is.
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(part, matchers.Reference):
|
|
97
|
+
return (part,)
|
|
98
|
+
vals = _values(part, settings, node, parents)
|
|
99
|
+
for v in vals:
|
|
100
|
+
if not isinstance(v, typ):
|
|
101
|
+
raise TypeError(
|
|
102
|
+
f'{owner} requires {typ.__name__}, got {type(v).__name__}'
|
|
103
|
+
f' — use $$(path|{typ.__name__}) to coerce')
|
|
104
|
+
return vals
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ContainerOp(base.Op):
|
|
108
|
+
"""
|
|
109
|
+
Base of the container value patterns. Their args are their parts, so
|
|
110
|
+
whether one holds a substitution or a reference is read from those.
|
|
111
|
+
|
|
112
|
+
A reference inside resolves as the whole value's would (see
|
|
113
|
+
results.guard_matcher). One that is a pattern branches the container:
|
|
114
|
+
branches() gives the container once per combination of values.
|
|
115
|
+
"""
|
|
116
|
+
def is_template(self):
|
|
117
|
+
"""
|
|
118
|
+
True if a part holds an unresolved substitution.
|
|
119
|
+
"""
|
|
120
|
+
return _holds(self.args, 'is_template')
|
|
121
|
+
|
|
122
|
+
def is_reference(self):
|
|
123
|
+
"""
|
|
124
|
+
True if a part holds an internal reference.
|
|
125
|
+
"""
|
|
126
|
+
return _holds(self.args, 'is_reference')
|
|
127
|
+
|
|
128
|
+
|
|
49
129
|
# ---------------------------------------------------------------------------
|
|
50
130
|
# Glob — the ... element
|
|
51
131
|
# ---------------------------------------------------------------------------
|
|
52
132
|
|
|
53
|
-
class Glob(
|
|
133
|
+
class Glob(ContainerOp):
|
|
54
134
|
"""
|
|
55
135
|
The ... element: zero or more values with optional pattern and count.
|
|
56
136
|
|
|
@@ -87,6 +167,14 @@ class Glob(base.Op):
|
|
|
87
167
|
return self
|
|
88
168
|
return Glob(new_pat, self.min_count, self.max_count)
|
|
89
169
|
|
|
170
|
+
def branches(self, settings, node=None, parents=()):
|
|
171
|
+
"""
|
|
172
|
+
This glob once per form of its pattern (see ContainerOp).
|
|
173
|
+
"""
|
|
174
|
+
return tuple(
|
|
175
|
+
Glob(p, self.min_count, self.max_count)
|
|
176
|
+
for p in _alternatives(self.pattern, settings, node, parents))
|
|
177
|
+
|
|
90
178
|
def __repr__(self):
|
|
91
179
|
parts = ['...']
|
|
92
180
|
if self.pattern is not None:
|
|
@@ -105,7 +193,7 @@ class Glob(base.Op):
|
|
|
105
193
|
# DictGlobEntry — a glob entry in a dict pattern
|
|
106
194
|
# ---------------------------------------------------------------------------
|
|
107
195
|
|
|
108
|
-
class DictGlobEntry(
|
|
196
|
+
class DictGlobEntry(ContainerOp):
|
|
109
197
|
"""
|
|
110
198
|
A glob entry in a dict: glob on key side with value pattern.
|
|
111
199
|
|
|
@@ -141,6 +229,15 @@ class DictGlobEntry(base.Op):
|
|
|
141
229
|
return self
|
|
142
230
|
return DictGlobEntry(new_kg, new_vp)
|
|
143
231
|
|
|
232
|
+
def branches(self, settings, node=None, parents=()):
|
|
233
|
+
"""
|
|
234
|
+
This entry once per pairing of its key glob's and value pattern's
|
|
235
|
+
forms (see ContainerOp).
|
|
236
|
+
"""
|
|
237
|
+
keys = _alternatives(self.key_glob, settings, node, parents)
|
|
238
|
+
vals = _alternatives(self.val_pattern, settings, node, parents)
|
|
239
|
+
return tuple(DictGlobEntry(k, v) for k, v in product(keys, vals))
|
|
240
|
+
|
|
144
241
|
def __repr__(self):
|
|
145
242
|
val_repr = repr(self.val_pattern) if self.val_pattern is not None else ''
|
|
146
243
|
key_repr = repr(self.key_glob)
|
|
@@ -156,7 +253,7 @@ class DictGlobEntry(base.Op):
|
|
|
156
253
|
_LIST_TYPES = (list, tuple)
|
|
157
254
|
|
|
158
255
|
|
|
159
|
-
class ContainerList(
|
|
256
|
+
class ContainerList(ContainerOp):
|
|
160
257
|
"""
|
|
161
258
|
List/tuple pattern: [elem, elem, ...].
|
|
162
259
|
|
|
@@ -213,6 +310,14 @@ class ContainerList(base.Op):
|
|
|
213
310
|
return self
|
|
214
311
|
return ContainerList(*new_elems, type_prefix=self.type_prefix)
|
|
215
312
|
|
|
313
|
+
def branches(self, settings, node=None, parents=()):
|
|
314
|
+
"""
|
|
315
|
+
This list once per combination of its elements' forms (see
|
|
316
|
+
ContainerOp).
|
|
317
|
+
"""
|
|
318
|
+
forms = (_alternatives(e, settings, node, parents) for e in self.elements)
|
|
319
|
+
return tuple(ContainerList(*c, type_prefix=self.type_prefix) for c in product(*forms))
|
|
320
|
+
|
|
216
321
|
def __repr__(self):
|
|
217
322
|
prefix = self.type_prefix or ''
|
|
218
323
|
return prefix + '[' + ', '.join(repr(e) for e in self.elements) + ']'
|
|
@@ -222,7 +327,7 @@ class ContainerList(base.Op):
|
|
|
222
327
|
# ContainerDict — {k: v, ...} pattern
|
|
223
328
|
# ---------------------------------------------------------------------------
|
|
224
329
|
|
|
225
|
-
class ContainerDict(
|
|
330
|
+
class ContainerDict(ContainerOp):
|
|
226
331
|
"""
|
|
227
332
|
Dict pattern: {key: val, key: val, ...}.
|
|
228
333
|
|
|
@@ -280,6 +385,14 @@ class ContainerDict(base.Op):
|
|
|
280
385
|
return self
|
|
281
386
|
return ContainerDict(*new_entries, type_prefix=self.type_prefix)
|
|
282
387
|
|
|
388
|
+
def branches(self, settings, node=None, parents=()):
|
|
389
|
+
"""
|
|
390
|
+
This dict once per combination of its entries' forms (see
|
|
391
|
+
ContainerOp).
|
|
392
|
+
"""
|
|
393
|
+
forms = (_alternatives(e, settings, node, parents) for e in self.entries)
|
|
394
|
+
return tuple(ContainerDict(*c, type_prefix=self.type_prefix) for c in product(*forms))
|
|
395
|
+
|
|
283
396
|
def matchable(self, op, specials=False):
|
|
284
397
|
"""
|
|
285
398
|
ContainerDict can match against Const values.
|
|
@@ -302,7 +415,7 @@ class ContainerDict(base.Op):
|
|
|
302
415
|
# ContainerSet — {v, v, ...} pattern
|
|
303
416
|
# ---------------------------------------------------------------------------
|
|
304
417
|
|
|
305
|
-
class ContainerSet(
|
|
418
|
+
class ContainerSet(ContainerOp):
|
|
306
419
|
"""
|
|
307
420
|
Set/frozenset pattern: {elem, elem, ...}.
|
|
308
421
|
|
|
@@ -353,6 +466,14 @@ class ContainerSet(base.Op):
|
|
|
353
466
|
return self
|
|
354
467
|
return ContainerSet(*new_elems, type_prefix=self.type_prefix)
|
|
355
468
|
|
|
469
|
+
def branches(self, settings, node=None, parents=()):
|
|
470
|
+
"""
|
|
471
|
+
This set once per combination of its elements' forms (see
|
|
472
|
+
ContainerOp).
|
|
473
|
+
"""
|
|
474
|
+
forms = (_alternatives(e, settings, node, parents) for e in self.elements)
|
|
475
|
+
return tuple(ContainerSet(*c, type_prefix=self.type_prefix) for c in product(*forms))
|
|
476
|
+
|
|
356
477
|
def matchable(self, op, specials=False):
|
|
357
478
|
"""
|
|
358
479
|
ContainerSet can match against Const values.
|
|
@@ -368,7 +489,7 @@ class ContainerSet(base.Op):
|
|
|
368
489
|
# StringGlob — "prefix"..."suffix" string pattern
|
|
369
490
|
# ---------------------------------------------------------------------------
|
|
370
491
|
|
|
371
|
-
class StringGlob(
|
|
492
|
+
class StringGlob(ContainerOp):
|
|
372
493
|
"""
|
|
373
494
|
String glob pattern: quoted fragments with ... between them.
|
|
374
495
|
|
|
@@ -437,7 +558,8 @@ class StringGlob(base.Op):
|
|
|
437
558
|
new_parts = []
|
|
438
559
|
changed = False
|
|
439
560
|
for p in self.parts:
|
|
440
|
-
|
|
561
|
+
# a glob holds no substitution; a reference resolves when matched
|
|
562
|
+
if isinstance(p, (Glob, matchers.Reference)) or not hasattr(p, 'resolve'):
|
|
441
563
|
new_parts.append(p)
|
|
442
564
|
continue
|
|
443
565
|
np = p.resolve(bindings, partial)
|
|
@@ -462,6 +584,14 @@ class StringGlob(base.Op):
|
|
|
462
584
|
return self
|
|
463
585
|
return StringGlob(*new_parts)
|
|
464
586
|
|
|
587
|
+
def branches(self, settings, node=None, parents=()):
|
|
588
|
+
"""
|
|
589
|
+
This glob once per combination of the strings its references
|
|
590
|
+
resolve to (see ContainerOp).
|
|
591
|
+
"""
|
|
592
|
+
forms = (_fragments(p, str, 'StringGlob', settings, node, parents) for p in self.parts)
|
|
593
|
+
return tuple(StringGlob(*c) for c in product(*forms))
|
|
594
|
+
|
|
465
595
|
def matches(self, vals):
|
|
466
596
|
"""
|
|
467
597
|
Yield str vals matching the glob pattern.
|
|
@@ -493,7 +623,7 @@ class StringGlob(base.Op):
|
|
|
493
623
|
# BytesGlob — b"prefix"...b"suffix" bytes pattern
|
|
494
624
|
# ---------------------------------------------------------------------------
|
|
495
625
|
|
|
496
|
-
class BytesGlob(
|
|
626
|
+
class BytesGlob(ContainerOp):
|
|
497
627
|
"""
|
|
498
628
|
Bytes glob pattern: byte-string fragments with ... between them.
|
|
499
629
|
|
|
@@ -562,7 +692,8 @@ class BytesGlob(base.Op):
|
|
|
562
692
|
new_parts = []
|
|
563
693
|
changed = False
|
|
564
694
|
for p in self.parts:
|
|
565
|
-
|
|
695
|
+
# a glob holds no substitution; a reference resolves when matched
|
|
696
|
+
if isinstance(p, (Glob, matchers.Reference)) or not hasattr(p, 'resolve'):
|
|
566
697
|
new_parts.append(p)
|
|
567
698
|
continue
|
|
568
699
|
np = p.resolve(bindings, partial)
|
|
@@ -587,6 +718,14 @@ class BytesGlob(base.Op):
|
|
|
587
718
|
return self
|
|
588
719
|
return BytesGlob(*new_parts)
|
|
589
720
|
|
|
721
|
+
def branches(self, settings, node=None, parents=()):
|
|
722
|
+
"""
|
|
723
|
+
This glob once per combination of the bytes its references
|
|
724
|
+
resolve to (see ContainerOp).
|
|
725
|
+
"""
|
|
726
|
+
forms = (_fragments(p, bytes, 'BytesGlob', settings, node, parents) for p in self.parts)
|
|
727
|
+
return tuple(BytesGlob(*c) for c in product(*forms))
|
|
728
|
+
|
|
590
729
|
def matches(self, vals):
|
|
591
730
|
"""
|
|
592
731
|
Yield bytes vals matching the glob pattern.
|
|
@@ -618,7 +757,7 @@ class BytesGlob(base.Op):
|
|
|
618
757
|
# ValueGroup — (val1, val2) value disjunction
|
|
619
758
|
# ---------------------------------------------------------------------------
|
|
620
759
|
|
|
621
|
-
class ValueGroup(
|
|
760
|
+
class ValueGroup(ContainerOp):
|
|
622
761
|
"""
|
|
623
762
|
Value group: (val1, val2, ...) — matches any alternative.
|
|
624
763
|
|
|
@@ -652,6 +791,15 @@ class ValueGroup(base.Op):
|
|
|
652
791
|
return self
|
|
653
792
|
return ValueGroup(*new_alts)
|
|
654
793
|
|
|
794
|
+
def branches(self, settings, node=None, parents=()):
|
|
795
|
+
"""
|
|
796
|
+
This group with its references resolved: one group, whose
|
|
797
|
+
alternatives are every form its alternatives take (see
|
|
798
|
+
ContainerOp).
|
|
799
|
+
"""
|
|
800
|
+
alts = (f for a in self.alternatives for f in _alternatives(a, settings, node, parents))
|
|
801
|
+
return (ValueGroup(*alts),)
|
|
802
|
+
|
|
655
803
|
def matchable(self, op, specials=False):
|
|
656
804
|
"""
|
|
657
805
|
ValueGroup can match against Const values.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Filter ops: key-value filters, boolean combinators, and negation.
|
|
3
3
|
"""
|
|
4
4
|
from . import base
|
|
5
|
+
from . import matchers
|
|
5
6
|
from . import predicates
|
|
6
7
|
from .access import Slot, Slice
|
|
7
8
|
|
|
@@ -112,29 +113,37 @@ class FilterKey(base.MatchOp):
|
|
|
112
113
|
if hasattr(part, 'gather'):
|
|
113
114
|
yield from self._get_values(part.gather(node, settings), rest, settings)
|
|
114
115
|
return
|
|
116
|
+
# a reference in a part resolves against the node the part is looked
|
|
117
|
+
# up in, as ^, and that node's ancestors above it
|
|
118
|
+
below = settings.below(node)
|
|
115
119
|
if isinstance(part, Slot):
|
|
120
|
+
# only a reference goes through settings: a resolver taking
|
|
121
|
+
# literals in segments is not offered a filter key's
|
|
122
|
+
slots = part._resolved(node=node, settings=settings) if part.is_reference() else (part,)
|
|
116
123
|
try:
|
|
117
|
-
for
|
|
118
|
-
|
|
124
|
+
for slot in slots:
|
|
125
|
+
for v in slot.values(node):
|
|
126
|
+
yield from self._get_values(v, rest, below)
|
|
119
127
|
except (TypeError, AttributeError):
|
|
120
128
|
yield None, False
|
|
121
129
|
return
|
|
122
130
|
if isinstance(part, Slice):
|
|
123
131
|
try:
|
|
124
|
-
s = part.
|
|
132
|
+
s = next(iter(part.keys(node, settings=settings)))
|
|
125
133
|
val = node[s]
|
|
126
|
-
yield from self._get_values(val, rest,
|
|
127
|
-
except (TypeError, AttributeError, KeyError, IndexError):
|
|
134
|
+
yield from self._get_values(val, rest, below)
|
|
135
|
+
except (StopIteration, TypeError, AttributeError, KeyError, IndexError):
|
|
128
136
|
yield None, False
|
|
129
137
|
return
|
|
130
138
|
# Key-like part (Word, Wildcard, etc.)
|
|
131
139
|
if not hasattr(node, 'keys'):
|
|
132
140
|
yield None, False
|
|
133
141
|
return
|
|
142
|
+
keys = (k for m in self._key_matchers(part, node, settings) for k in m.matches(node.keys()))
|
|
134
143
|
if len(parts) == 1 and not isinstance(part, Slot):
|
|
135
144
|
# Simple key - yield all matching values (existing single-key behavior)
|
|
136
145
|
found_any = False
|
|
137
|
-
for km in
|
|
146
|
+
for km in keys:
|
|
138
147
|
yield node[km], True
|
|
139
148
|
found_any = True
|
|
140
149
|
if not found_any:
|
|
@@ -142,14 +151,31 @@ class FilterKey(base.MatchOp):
|
|
|
142
151
|
return
|
|
143
152
|
# Dotted path - traverse (first match only at each key-like level)
|
|
144
153
|
found = False
|
|
145
|
-
for km in
|
|
154
|
+
for km in keys:
|
|
146
155
|
child = node[km]
|
|
147
|
-
yield from self._get_values(child, rest,
|
|
156
|
+
yield from self._get_values(child, rest, below)
|
|
148
157
|
found = True
|
|
149
158
|
break
|
|
150
159
|
if not found:
|
|
151
160
|
yield None, False
|
|
152
161
|
|
|
162
|
+
def _key_matchers(self, part, node, settings):
|
|
163
|
+
"""
|
|
164
|
+
The match ops a key-like part tests node's keys with: a reference
|
|
165
|
+
resolved against node and its ancestors, one per value when its
|
|
166
|
+
path is a pattern, none when it does not resolve; the part itself
|
|
167
|
+
otherwise.
|
|
168
|
+
"""
|
|
169
|
+
if not part.is_reference():
|
|
170
|
+
return (part,)
|
|
171
|
+
try:
|
|
172
|
+
val = settings.resolve(part, node, settings.parents or ())
|
|
173
|
+
except KeyError:
|
|
174
|
+
return ()
|
|
175
|
+
if part.is_pattern():
|
|
176
|
+
return tuple(matchers.ResolvedValue(v) for v in val)
|
|
177
|
+
return (matchers.ResolvedValue(val),)
|
|
178
|
+
|
|
153
179
|
def get_value(self, node):
|
|
154
180
|
"""
|
|
155
181
|
Get first matching value from node (backwards compat)
|
|
@@ -260,8 +260,11 @@ regex = _regex.copy().set_parse_action(matchers.Regex)
|
|
|
260
260
|
regex_first = (_regex + pp.Suppress(pp.Literal('?'))).set_parse_action(matchers.RegexFirst)
|
|
261
261
|
_escaped_dollar = pp.Regex(r'\\\$\${0,2}(\([^)]*\)|[0-9]*)').set_parse_action(
|
|
262
262
|
lambda t: matchers.Word(t[0][1:]))
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
# $$(path) runs to the ) that closes it, so the path may hold parentheses
|
|
264
|
+
# of its own, $$((a[*];)) or $$(a.(b,c)); a ) inside quotes does not count.
|
|
265
|
+
_reference = (S(pp.Regex(r'\$\$(?=\()'))
|
|
266
|
+
+ pp.original_text_for(pp.nested_expr('(', ')')).add_condition(lambda t: t[0] != '()')
|
|
267
|
+
).set_parse_action(lambda t: matchers.Reference(t[0][1:-1]))
|
|
265
268
|
def _paren_subst_action(t):
|
|
266
269
|
"""
|
|
267
270
|
Parse $(content) where content is name_or_index[|transform1|transform2...].
|
|
@@ -285,8 +288,11 @@ var = _escaped_dollar | _reference | _paren_subst | _raw_subst | _dollar_word
|
|
|
285
288
|
# |min:$amount.value or =^lots.taken: one string, for a resolver to read
|
|
286
289
|
# whole. In a segment position the . separates segments, so not there.
|
|
287
290
|
_arg_sigil = pp.Regex(r'(?:\$(?![0-9($])|\^)[^' + re.escape(reserved.replace('.', '')) + r' \t\n\r]+').set_parse_action(matchers.Word)
|
|
288
|
-
slice
|
|
289
|
-
|
|
291
|
+
# A slice bound: an integer, + (the length), or a $$(path) reference or
|
|
292
|
+
# $(name)/$N substitution standing for one.
|
|
293
|
+
_slice_bound = integer | plus | _reference | _paren_subst | _raw_subst
|
|
294
|
+
slice = pp.Optional(_slice_bound) + ':' + pp.Optional(_slice_bound) \
|
|
295
|
+
+ pp.Optional(':') + pp.Optional(_slice_bound)
|
|
290
296
|
|
|
291
297
|
_common_pats = wildcard_first | wildcard | var | regex_first | regex
|
|
292
298
|
_commons = bytes_literal | string | _common_pats | numeric_quoted
|
|
@@ -92,9 +92,33 @@ class Recursive(BaseOp):
|
|
|
92
92
|
s = '.' + s
|
|
93
93
|
return s
|
|
94
94
|
|
|
95
|
+
def _parts(self):
|
|
96
|
+
"""
|
|
97
|
+
The inner match op, the accessor ops and the filters: every part
|
|
98
|
+
that may hold a substitution or reference.
|
|
99
|
+
"""
|
|
100
|
+
yield self.inner
|
|
101
|
+
for item in self.accessors or ():
|
|
102
|
+
if item is base.BRANCH_CUT or item is base.BRANCH_SOFTCUT:
|
|
103
|
+
continue
|
|
104
|
+
yield from item
|
|
105
|
+
yield from self.filters
|
|
106
|
+
|
|
107
|
+
def is_template(self):
|
|
108
|
+
"""
|
|
109
|
+
True if a part contains a substitution reference.
|
|
110
|
+
"""
|
|
111
|
+
return any(hasattr(p, 'is_template') and p.is_template() for p in self._parts())
|
|
112
|
+
|
|
113
|
+
def is_reference(self):
|
|
114
|
+
"""
|
|
115
|
+
True if a part contains an internal reference.
|
|
116
|
+
"""
|
|
117
|
+
return any(hasattr(p, 'is_reference') and p.is_reference() for p in self._parts())
|
|
118
|
+
|
|
95
119
|
def resolve(self, bindings, partial=False):
|
|
96
120
|
"""
|
|
97
|
-
Resolve $N in inner match op
|
|
121
|
+
Resolve $N in inner match op, accessor branches and filters.
|
|
98
122
|
"""
|
|
99
123
|
new_inner = self.inner.resolve(bindings, partial) if hasattr(self.inner, 'resolve') else self.inner
|
|
100
124
|
new_accessors = None
|
|
@@ -111,11 +135,16 @@ class Recursive(BaseOp):
|
|
|
111
135
|
acc_changed = True
|
|
112
136
|
new_accessors.append(new_branch)
|
|
113
137
|
new_accessors = tuple(new_accessors)
|
|
114
|
-
|
|
138
|
+
new_filters = tuple(f.resolve(bindings, partial) for f in self.filters)
|
|
139
|
+
filt_changed = not all(nf is of for nf, of in zip(new_filters, self.filters))
|
|
140
|
+
if new_inner is self.inner and not acc_changed and not filt_changed:
|
|
115
141
|
return self
|
|
116
|
-
|
|
142
|
+
# type(self): a RecursiveFirst stays first-match
|
|
143
|
+
r = type(self)(
|
|
117
144
|
new_inner, accessors=new_accessors if new_accessors is not None else self.accessors,
|
|
118
145
|
depth_start=self.depth_start, depth_stop=self.depth_stop, depth_step=self.depth_step)
|
|
146
|
+
r.filters = new_filters
|
|
147
|
+
return r
|
|
119
148
|
|
|
120
149
|
def match(self, op, specials=False):
|
|
121
150
|
return self.inner.matchable(op, specials=specials)
|
|
@@ -371,7 +400,7 @@ class Recursive(BaseOp):
|
|
|
371
400
|
node[k] = v
|
|
372
401
|
return node
|
|
373
402
|
|
|
374
|
-
def _update_recursive(self, ops, node, val, has_defaults, _path, nop, depth=0, guard=None, seen=frozenset(), settings=base.SETTINGS):
|
|
403
|
+
def _update_recursive(self, ops, node, val, has_defaults, _path, nop, depth=0, guard=None, seen=frozenset(), settings=base.SETTINGS, nop_from_unwrap=False):
|
|
375
404
|
node_id = id(node)
|
|
376
405
|
if node_id in seen:
|
|
377
406
|
return node
|
|
@@ -382,7 +411,8 @@ class Recursive(BaseOp):
|
|
|
382
411
|
|
|
383
412
|
for acc, k, v in items:
|
|
384
413
|
# Recurse first (bottom-up)
|
|
385
|
-
v = self._update_recursive(ops, v, val, has_defaults, _path, nop, depth + 1, guard, seen, settings=settings
|
|
414
|
+
v = self._update_recursive(ops, v, val, has_defaults, _path, nop, depth + 1, guard, seen, settings=settings,
|
|
415
|
+
nop_from_unwrap=nop_from_unwrap)
|
|
386
416
|
node = self._assign(acc, node, k, v)
|
|
387
417
|
if self.filters and not any(True for _ in self.filtered((v,), settings)):
|
|
388
418
|
continue
|
|
@@ -392,13 +422,15 @@ class Recursive(BaseOp):
|
|
|
392
422
|
if guard and not guard(v):
|
|
393
423
|
continue
|
|
394
424
|
if ops:
|
|
395
|
-
|
|
425
|
+
# a ~ on this op holds back only this op, not the ops after it
|
|
426
|
+
pass_nop = nop and not nop_from_unwrap
|
|
427
|
+
node = self._assign(acc, node, k, engine.updates(ops, v, val, has_defaults, _path + [(self, k)], pass_nop, settings=settings))
|
|
396
428
|
elif not nop:
|
|
397
429
|
node = self._assign(acc, node, k, val)
|
|
398
430
|
return node
|
|
399
431
|
|
|
400
|
-
def do_update(self, ops, node, val, has_defaults, _path, nop, settings=base.SETTINGS):
|
|
401
|
-
return self._update_recursive(ops, node, val, has_defaults, _path, nop, settings=settings)
|
|
432
|
+
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, settings=base.SETTINGS):
|
|
433
|
+
return self._update_recursive(ops, node, val, has_defaults, _path, nop, settings=settings, nop_from_unwrap=nop_from_unwrap)
|
|
402
434
|
|
|
403
435
|
def _remove_recursive(self, ops, node, val, nop, depth=0, guard=None, seen=frozenset(), settings=base.SETTINGS):
|
|
404
436
|
node_id = id(node)
|
|
@@ -264,16 +264,23 @@ def guard_matcher(guard, settings=base.SETTINGS, node=None, parents=(), position
|
|
|
264
264
|
$$(^x) looks x up in it). A plain value is resolved too when the
|
|
265
265
|
resolver takes literals at position, 'guards' or 'filters'. A reference
|
|
266
266
|
path that is a pattern becomes a value group of what it resolves to.
|
|
267
|
+
A container value resolves the references inside it the same way; one
|
|
268
|
+
that is a pattern branches the container, so it becomes a value group
|
|
269
|
+
of the container once per value (see containers.ContainerOp).
|
|
267
270
|
Raises UnresolvedReference when the reference does not resolve.
|
|
268
271
|
"""
|
|
269
|
-
# container values (ContainerList and kin) are ops without is_reference
|
|
270
272
|
reference = hasattr(guard, 'is_reference') and guard.is_reference()
|
|
271
273
|
if not (reference or (settings.literals(position) and isinstance(guard, Const))):
|
|
272
274
|
return guard
|
|
275
|
+
from . import containers
|
|
276
|
+
if isinstance(guard, containers.ContainerOp):
|
|
277
|
+
branches = guard.branches(settings, node, parents)
|
|
278
|
+
if len(branches) == 1:
|
|
279
|
+
return branches[0]
|
|
280
|
+
return containers.ValueGroup(*branches)
|
|
273
281
|
val = settings.resolve(guard, node, parents)
|
|
274
282
|
if not guard.is_pattern():
|
|
275
283
|
return Const(val)
|
|
276
|
-
from . import containers
|
|
277
284
|
return containers.ValueGroup(*(Const(v) for v in val))
|
|
278
285
|
|
|
279
286
|
|
|
@@ -314,7 +314,7 @@ class TransformWrap(Wrap):
|
|
|
314
314
|
val = self.apply(val, settings)
|
|
315
315
|
except base.UnresolvedReference:
|
|
316
316
|
return node
|
|
317
|
-
return self.inner.do_update(ops, node, val, has_defaults, _path, nop, settings=settings)
|
|
317
|
+
return self.inner.do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap, settings=settings)
|
|
318
318
|
|
|
319
319
|
def do_remove(self, ops, node, val, nop, settings=base.SETTINGS):
|
|
320
320
|
return self.inner.do_remove(ops, node, val, nop, settings=settings)
|
|
@@ -543,7 +543,8 @@ class ValueGuard(Wrap):
|
|
|
543
543
|
nop = self._nop(nop)
|
|
544
544
|
if self.inner.is_recursive():
|
|
545
545
|
return self.inner._update_recursive(
|
|
546
|
-
ops, node, val, has_defaults, _path, nop, guard=self._guard(settings), settings=settings
|
|
546
|
+
ops, node, val, has_defaults, _path, nop, guard=self._guard(settings), settings=settings,
|
|
547
|
+
nop_from_unwrap=nop_from_unwrap)
|
|
547
548
|
if not ops and not nop:
|
|
548
549
|
# the leaf: upsert() has no settings, and the guard needs them
|
|
549
550
|
for k in list(self.keys(node, settings=settings)):
|
|
@@ -788,8 +789,9 @@ class FilterWrap(Wrap):
|
|
|
788
789
|
return self
|
|
789
790
|
return FilterWrap(new_inner, new_filters)
|
|
790
791
|
|
|
791
|
-
def do_update(self, ops, node, val, has_defaults, _path, nop, settings=base.SETTINGS):
|
|
792
|
-
return access.BaseOp.do_update(
|
|
792
|
+
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, settings=base.SETTINGS):
|
|
793
|
+
return access.BaseOp.do_update(
|
|
794
|
+
self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap, settings=settings)
|
|
793
795
|
|
|
794
796
|
def do_remove(self, ops, node, val, nop, settings=base.SETTINGS):
|
|
795
797
|
return access.BaseOp.do_remove(self, ops, node, val, nop, settings=settings)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.49.
|
|
3
|
+
Version: 0.49.4
|
|
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
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
24
25
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
26
|
Classifier: Topic :: Utilities
|
|
26
27
|
Requires-Python: >=3.6
|
|
@@ -1163,6 +1164,14 @@ Dotted slicing works like python slicing and all that entails.
|
|
|
1163
1164
|
>>> dotted.get(d, '*.there[1:]')
|
|
1164
1165
|
([2, 3], [5, 6])
|
|
1165
1166
|
|
|
1167
|
+
A bound may be a reference or a substitution, resolved the way a slot's is:
|
|
1168
|
+
|
|
1169
|
+
>>> d = {'b': [3, 0, 5], 'i': 2}
|
|
1170
|
+
>>> dotted.get(d, 'b[:$$(i)]')
|
|
1171
|
+
[3, 0]
|
|
1172
|
+
>>> dotted.get(d, 'b[$(n):]', bindings={'n': 1})
|
|
1173
|
+
[0, 5]
|
|
1174
|
+
|
|
1166
1175
|
<a id="dot-notation-for-sequence-indexing"></a>
|
|
1167
1176
|
### Dot notation for sequence indexing
|
|
1168
1177
|
|
|
@@ -1641,6 +1650,23 @@ as do the transforms before a guard:
|
|
|
1641
1650
|
>>> dotted.get(data, '*|mul:$$(scale)=20')
|
|
1642
1651
|
(2,)
|
|
1643
1652
|
|
|
1653
|
+
So does a reference inside a container value. One whose path is a pattern
|
|
1654
|
+
branches the container, once per value; pack it, `(…;)`, to compare with the
|
|
1655
|
+
tuple instead:
|
|
1656
|
+
|
|
1657
|
+
>>> data = {'want': [2, 3], 'items': [[1, 2], [1, 4], [1, (2, 3)]]}
|
|
1658
|
+
>>> dotted.get(data, 'items[*]=[1, $$(want[*])]')
|
|
1659
|
+
([1, 2],)
|
|
1660
|
+
>>> dotted.get(data, 'items[*]=[1, $$((want[*];))]')
|
|
1661
|
+
([1, (2, 3)],)
|
|
1662
|
+
|
|
1663
|
+
A filter key may hold references too. In each part of the key, `^` is the
|
|
1664
|
+
node that part is looked up in, so in the first part it is the item:
|
|
1665
|
+
|
|
1666
|
+
>>> data = {'field': 'name', 'users': [{'name': 'alice'}, {'name': 'bob'}]}
|
|
1667
|
+
>>> dotted.get(data, 'users[*&$$(field)="bob"]')
|
|
1668
|
+
({'name': 'bob'},)
|
|
1669
|
+
|
|
1644
1670
|
With `update`, the transforms apply to the value being set, so `^` is that
|
|
1645
1671
|
value:
|
|
1646
1672
|
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "dotted_notation"
|
|
9
|
-
version = "0.49.
|
|
9
|
+
version = "0.49.4"
|
|
10
10
|
description = "Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.6"
|
|
@@ -38,6 +38,7 @@ classifiers = [
|
|
|
38
38
|
"Programming Language :: Python :: 3.12",
|
|
39
39
|
"Programming Language :: Python :: 3.13",
|
|
40
40
|
"Programming Language :: Python :: 3.14",
|
|
41
|
+
"Programming Language :: Python :: 3.15",
|
|
41
42
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
42
43
|
"Topic :: Utilities",
|
|
43
44
|
]
|
|
@@ -78,7 +79,7 @@ Changelog = "https://github.com/freywaid/dotted/blob/master/CHANGELOG.md"
|
|
|
78
79
|
include = ["dotted*"]
|
|
79
80
|
|
|
80
81
|
[tool.cibuildwheel]
|
|
81
|
-
# Compiled wheels for CPython 3.11-3.
|
|
82
|
+
# Compiled wheels for CPython 3.11-3.15 (the versions are listed in the
|
|
82
83
|
# publish workflow, one job each); everything else installs the pure wheel.
|
|
83
84
|
skip = "pp*"
|
|
84
85
|
test-requires = "pytest"
|
|
@@ -64,4 +64,7 @@ class build_ext(_build_ext):
|
|
|
64
64
|
self.parallel = os.cpu_count() or 1
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
# Guarded: cythonize compiles in a process pool, and a spawned worker
|
|
68
|
+
# (macOS, and Linux from Python 3.14) imports this file again.
|
|
69
|
+
if __name__ == '__main__':
|
|
70
|
+
setup(ext_modules=extensions(), cmdclass={'build_ext': build_ext})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dotted_notation-0.49.2 → dotted_notation-0.49.4}/dotted_notation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|