dotted-notation 0.44.4__tar.gz → 0.44.6__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.44.4 → dotted_notation-0.44.6}/CHANGELOG.md +32 -0
- {dotted_notation-0.44.4/dotted_notation.egg-info → dotted_notation-0.44.6}/PKG-INFO +1 -1
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/api.py +28 -52
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/base.py +40 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/groups.py +36 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/recursive.py +22 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/results.py +6 -5
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/wrappers.py +12 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6/dotted_notation.egg-info}/PKG-INFO +1 -1
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/pyproject.toml +1 -1
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/LICENSE +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/MANIFEST.in +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/README.md +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/__init__.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/__main__.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/access.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/cli/main.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/containers.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/engine.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/filters.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/grammar.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/matchers.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/predicates.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/sql/core.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/sql/pg.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/transforms.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/utils.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/utypes.py +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.44.4 → dotted_notation-0.44.6}/setup.cfg +0 -0
|
@@ -3,6 +3,38 @@
|
|
|
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.44.6]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- `match()` supports op groups: disjunction `(a,b)`, first-match `(a,b)?`,
|
|
10
|
+
conjunction `(a&b)`, and negation `(!a)` now match paths instead of
|
|
11
|
+
raising `AttributeError`. A path matches a group if it matches any
|
|
12
|
+
branch followed by the rest of the pattern (a concrete path is only
|
|
13
|
+
ever one branch's output, so Or/First/And all reduce to this);
|
|
14
|
+
negation matches one segment its inner pattern does not.
|
|
15
|
+
Multi-segment branches, nested groups, recursive ops inside branches,
|
|
16
|
+
cut markers, and `groups=True` captures all supported.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Wrapped variadic patterns in `match()`: `~(a,b)` (nop-wrapped group)
|
|
20
|
+
silently matched nothing and `**=7` (value-guarded recursive) raised
|
|
21
|
+
`AttributeError`; both now match.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Path matching is polymorphic: `base.match_ops()` dispatches to each
|
|
25
|
+
op's `do_match`, which decides how many path segments it consumes
|
|
26
|
+
(single-segment default; backtracking on `Recursive`; branch
|
|
27
|
+
expansion on groups; `Wrap` delegates to variadic inners). The api
|
|
28
|
+
layer no longer special-cases op types.
|
|
29
|
+
|
|
30
|
+
## [0.44.5]
|
|
31
|
+
|
|
32
|
+
### Performance
|
|
33
|
+
- Transforms no longer disqualify the get() fast path: `a.b|int`
|
|
34
|
+
resolves via the direct-lookup chain and applies transforms on the
|
|
35
|
+
hit (~7x faster than walk). Plain paths are unaffected; `is_simple()`
|
|
36
|
+
semantics unchanged (still requires no transforms).
|
|
37
|
+
|
|
6
38
|
## [0.44.4]
|
|
7
39
|
|
|
8
40
|
### Added
|
|
@@ -287,6 +287,8 @@ def is_simple(path):
|
|
|
287
287
|
False
|
|
288
288
|
"""
|
|
289
289
|
parsed = path if isinstance(path, results.Dotted) else parse(path)
|
|
290
|
+
if parsed.transforms:
|
|
291
|
+
return False
|
|
290
292
|
return parsed.simple_chain is not None
|
|
291
293
|
|
|
292
294
|
|
|
@@ -451,7 +453,11 @@ def get(obj, path, default=None, pattern_default=(), apply_transforms=True, stri
|
|
|
451
453
|
if chain is not None:
|
|
452
454
|
val = engine.simple_get(chain, obj, strict=strict)
|
|
453
455
|
if val is not engine.SIMPLE_BAIL:
|
|
454
|
-
|
|
456
|
+
if val is base.marker:
|
|
457
|
+
return default
|
|
458
|
+
if apply_transforms and ops.transforms:
|
|
459
|
+
return ops.apply(val)
|
|
460
|
+
return val
|
|
455
461
|
vals = engine.iter_until_cut(engine.gets(ops, obj, strict=strict))
|
|
456
462
|
if apply_transforms:
|
|
457
463
|
vals = ( ops.apply(v) for v in vals )
|
|
@@ -716,51 +722,6 @@ def remove_multi(obj, iterable, paths_only=True, mutable=True, strict=False, bin
|
|
|
716
722
|
return remove_if_multi(obj, iterable, paths_only=False, pred=None, mutable=mutable, strict=strict, bindings=bindings)
|
|
717
723
|
|
|
718
724
|
|
|
719
|
-
def _match_ops(pats, path_ops, partial):
|
|
720
|
-
"""
|
|
721
|
-
Recursive match of pattern ops against path ops.
|
|
722
|
-
Returns list of match values on success, None on failure.
|
|
723
|
-
Handles Recursive ops which can consume variable-length path segments.
|
|
724
|
-
"""
|
|
725
|
-
if not pats:
|
|
726
|
-
if not path_ops:
|
|
727
|
-
return []
|
|
728
|
-
if partial:
|
|
729
|
-
return []
|
|
730
|
-
return None
|
|
731
|
-
|
|
732
|
-
pop = pats[0]
|
|
733
|
-
rest_pats = pats[1:]
|
|
734
|
-
|
|
735
|
-
# Non-recursive op: consume exactly one path segment
|
|
736
|
-
if not pop.is_recursive():
|
|
737
|
-
if not path_ops:
|
|
738
|
-
return None
|
|
739
|
-
kop = path_ops[0]
|
|
740
|
-
m = pop.match(kop, specials=True)
|
|
741
|
-
if not m:
|
|
742
|
-
return None
|
|
743
|
-
rest_result = _match_ops(rest_pats, path_ops[1:], partial)
|
|
744
|
-
if rest_result is None:
|
|
745
|
-
return None
|
|
746
|
-
if isinstance(m, (tuple, list)):
|
|
747
|
-
return [_m.val for _m in m] + rest_result
|
|
748
|
-
return [m.val] + rest_result
|
|
749
|
-
|
|
750
|
-
# Recursive op: try consuming 1, 2, ... N path segments via backtracking
|
|
751
|
-
for n in range(1, len(path_ops) + 1):
|
|
752
|
-
kop = path_ops[n - 1]
|
|
753
|
-
seg_val = getattr(getattr(kop, 'op', kop), 'value', kop)
|
|
754
|
-
matched = any(True for _ in pop.inner.matches((seg_val,)))
|
|
755
|
-
if not matched:
|
|
756
|
-
break # chain-following: stop extending once a segment fails
|
|
757
|
-
rest_result = _match_ops(rest_pats, path_ops[n:], partial)
|
|
758
|
-
if rest_result is not None:
|
|
759
|
-
combined = results.assemble(path_ops[:n])
|
|
760
|
-
return [combined] + rest_result
|
|
761
|
-
return None
|
|
762
|
-
|
|
763
|
-
|
|
764
725
|
def match(pattern, path, groups=False, partial=True, strict=False):
|
|
765
726
|
"""
|
|
766
727
|
Returns `path` if `pattern` matches; otherwise `None`
|
|
@@ -785,6 +746,22 @@ def match(pattern, path, groups=False, partial=True, strict=False):
|
|
|
785
746
|
>>> match('hello.*', 'hello.there.bye', groups='patterns')
|
|
786
747
|
('hello.there.bye', ('there.bye',))
|
|
787
748
|
|
|
749
|
+
Group patterns match if any branch matches:
|
|
750
|
+
>>> match('(a,b)', 'a')
|
|
751
|
+
'a'
|
|
752
|
+
>>> match('(a,b)', 'c')
|
|
753
|
+
>>> match('x.(a,b)', 'x.b')
|
|
754
|
+
'x.b'
|
|
755
|
+
>>> match('(a.b,c)', 'a.b')
|
|
756
|
+
'a.b'
|
|
757
|
+
>>> match('(a,b)?', 'b')
|
|
758
|
+
'b'
|
|
759
|
+
>>> match('x(.a&.b)', 'x.a')
|
|
760
|
+
'x.a'
|
|
761
|
+
>>> match('(!a)', 'b')
|
|
762
|
+
'b'
|
|
763
|
+
>>> match('(!a)', 'a')
|
|
764
|
+
|
|
788
765
|
Recursive patterns:
|
|
789
766
|
>>> match('**.c', 'a.b.c')
|
|
790
767
|
'a.b.c'
|
|
@@ -806,14 +783,13 @@ def match(pattern, path, groups=False, partial=True, strict=False):
|
|
|
806
783
|
pats = parse(pattern)
|
|
807
784
|
path_ops = parse(path)
|
|
808
785
|
|
|
809
|
-
#
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
result = _match_ops(list(pats), list(path_ops), partial)
|
|
786
|
+
# Variadic ops (recursive, groups) consume variable-length path
|
|
787
|
+
# segments — use the recursive matcher
|
|
788
|
+
if any(op.is_variadic() for op in pats):
|
|
789
|
+
result = base.match_ops(list(pats), list(path_ops), partial)
|
|
814
790
|
if result is None:
|
|
815
791
|
return returns(None, [])
|
|
816
|
-
# TODO: pattern-only filtering for
|
|
792
|
+
# TODO: pattern-only filtering for variadic matches
|
|
817
793
|
return returns(path, result)
|
|
818
794
|
|
|
819
795
|
# Original non-recursive match logic
|
|
@@ -42,6 +42,22 @@ def has_any(gen):
|
|
|
42
42
|
return any(True for _ in gen)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def match_ops(pats, path_ops, partial):
|
|
46
|
+
"""
|
|
47
|
+
Match a list of pattern ops against a list of path ops.
|
|
48
|
+
Returns a list of match values on success, None on failure.
|
|
49
|
+
Dispatches to each op's do_match, which decides how many path
|
|
50
|
+
segments it consumes.
|
|
51
|
+
"""
|
|
52
|
+
if pats:
|
|
53
|
+
return pats[0].do_match(pats[1:], path_ops, partial)
|
|
54
|
+
if not path_ops:
|
|
55
|
+
return []
|
|
56
|
+
if partial:
|
|
57
|
+
return []
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
45
61
|
class MatchResult:
|
|
46
62
|
def __init__(self, val):
|
|
47
63
|
self.val = val
|
|
@@ -75,6 +91,12 @@ class Op:
|
|
|
75
91
|
return False
|
|
76
92
|
def is_slice(self):
|
|
77
93
|
return False
|
|
94
|
+
def is_variadic(self):
|
|
95
|
+
"""
|
|
96
|
+
True if this op can consume a variable number of path segments when
|
|
97
|
+
matching a path (recursive ops, groups).
|
|
98
|
+
"""
|
|
99
|
+
return False
|
|
78
100
|
|
|
79
101
|
|
|
80
102
|
class MetaNOP(type):
|
|
@@ -185,6 +207,24 @@ class TraversalOp(Op):
|
|
|
185
207
|
"""
|
|
186
208
|
return False
|
|
187
209
|
|
|
210
|
+
def do_match(self, rest_pats, path_ops, partial):
|
|
211
|
+
"""
|
|
212
|
+
Match this op against exactly one path segment, then continue with
|
|
213
|
+
the remaining pattern ops. Variadic ops (recursive, groups)
|
|
214
|
+
override to consume a variable number of segments.
|
|
215
|
+
"""
|
|
216
|
+
if not path_ops:
|
|
217
|
+
return None
|
|
218
|
+
m = self.match(path_ops[0], specials=True)
|
|
219
|
+
if not m:
|
|
220
|
+
return None
|
|
221
|
+
rest = match_ops(rest_pats, path_ops[1:], partial)
|
|
222
|
+
if rest is None:
|
|
223
|
+
return None
|
|
224
|
+
if isinstance(m, (tuple, list)):
|
|
225
|
+
return [_m.val for _m in m] + rest
|
|
226
|
+
return [m.val] + rest
|
|
227
|
+
|
|
188
228
|
|
|
189
229
|
class MatchOp(Op):
|
|
190
230
|
"""
|
|
@@ -32,6 +32,9 @@ class OpGroup(base.TraversalOp):
|
|
|
32
32
|
def is_pattern(self):
|
|
33
33
|
return True
|
|
34
34
|
|
|
35
|
+
def is_variadic(self):
|
|
36
|
+
return True
|
|
37
|
+
|
|
35
38
|
def is_template(self):
|
|
36
39
|
"""
|
|
37
40
|
True if any branch contains a template op.
|
|
@@ -99,6 +102,20 @@ class OpGroup(base.TraversalOp):
|
|
|
99
102
|
def operator(self, top=False):
|
|
100
103
|
return self._render(top)
|
|
101
104
|
|
|
105
|
+
def do_match(self, rest_pats, path_ops, partial):
|
|
106
|
+
"""
|
|
107
|
+
Match a concrete path against this group: the path matches if it
|
|
108
|
+
matches any branch followed by the remaining pattern ops. A single
|
|
109
|
+
concrete path can only be produced by one branch at a time, so
|
|
110
|
+
disjunction, first-match, and conjunction all reduce to "any branch
|
|
111
|
+
matches"; cut markers don't constrain matching either.
|
|
112
|
+
"""
|
|
113
|
+
for branch in base.branches_only(self.branches):
|
|
114
|
+
result = base.match_ops(list(branch) + list(rest_pats), path_ops, partial)
|
|
115
|
+
if result is not None:
|
|
116
|
+
return result
|
|
117
|
+
return None
|
|
118
|
+
|
|
102
119
|
def _render(self, top=True):
|
|
103
120
|
"""
|
|
104
121
|
Render the group as a string. Subclasses override this.
|
|
@@ -518,6 +535,25 @@ class OpGroupNot(OpGroup):
|
|
|
518
535
|
stack.push(base.Frame(frame.ops, v, cp, kwargs=frame.kwargs))
|
|
519
536
|
return ()
|
|
520
537
|
|
|
538
|
+
def do_match(self, rest_pats, path_ops, partial):
|
|
539
|
+
"""
|
|
540
|
+
Match one path segment NOT matched by the inner pattern's first op,
|
|
541
|
+
then continue with the rest of the inner branch and remaining ops.
|
|
542
|
+
"""
|
|
543
|
+
inner = self.inner
|
|
544
|
+
if not inner:
|
|
545
|
+
return None
|
|
546
|
+
if not path_ops:
|
|
547
|
+
return None
|
|
548
|
+
kop = path_ops[0]
|
|
549
|
+
if base.match_ops([inner[0]], [kop], False) is not None:
|
|
550
|
+
return None
|
|
551
|
+
rest = base.match_ops(list(inner[1:]) + list(rest_pats), path_ops[1:], partial)
|
|
552
|
+
if rest is None:
|
|
553
|
+
return None
|
|
554
|
+
seg_val = getattr(getattr(kop, 'op', kop), 'value', kop)
|
|
555
|
+
return [seg_val] + rest
|
|
556
|
+
|
|
521
557
|
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, **kwargs):
|
|
522
558
|
inner = self.inner
|
|
523
559
|
if not inner:
|
|
@@ -64,6 +64,9 @@ class Recursive(BaseOp):
|
|
|
64
64
|
def is_recursive(self):
|
|
65
65
|
return True
|
|
66
66
|
|
|
67
|
+
def is_variadic(self):
|
|
68
|
+
return True
|
|
69
|
+
|
|
67
70
|
def operator(self, top=False):
|
|
68
71
|
if self.accessors is not None:
|
|
69
72
|
s = f'*({self._render_accessors()})'
|
|
@@ -114,6 +117,25 @@ class Recursive(BaseOp):
|
|
|
114
117
|
def match(self, op, specials=False):
|
|
115
118
|
return self.inner.matchable(op, specials=specials)
|
|
116
119
|
|
|
120
|
+
def do_match(self, rest_pats, path_ops, partial):
|
|
121
|
+
"""
|
|
122
|
+
Match by consuming 1..N path segments via backtracking. Each
|
|
123
|
+
consumed segment must match the inner pattern (chain-following);
|
|
124
|
+
stop extending once a segment fails.
|
|
125
|
+
"""
|
|
126
|
+
from . import results
|
|
127
|
+
for n in range(1, len(path_ops) + 1):
|
|
128
|
+
kop = path_ops[n - 1]
|
|
129
|
+
seg_val = getattr(getattr(kop, 'op', kop), 'value', kop)
|
|
130
|
+
if not any(True for _ in self.inner.matches((seg_val,))):
|
|
131
|
+
return None
|
|
132
|
+
rest = base.match_ops(rest_pats, path_ops[n:], partial)
|
|
133
|
+
if rest is None:
|
|
134
|
+
continue
|
|
135
|
+
combined = results.assemble(path_ops[:n])
|
|
136
|
+
return [combined] + rest
|
|
137
|
+
return None
|
|
138
|
+
|
|
117
139
|
def _effective_branches(self):
|
|
118
140
|
"""
|
|
119
141
|
Return accessor branches that drive recursion.
|
|
@@ -113,13 +113,14 @@ class Dotted:
|
|
|
113
113
|
@lazyprop
|
|
114
114
|
def simple_chain(self):
|
|
115
115
|
"""
|
|
116
|
-
Tuple of (kind, key) pairs when
|
|
117
|
-
of concrete Key/Attr/Slot accesses with no patterns,
|
|
118
|
-
references, guards,
|
|
119
|
-
|
|
116
|
+
Tuple of (kind, key) pairs when the access chain is simple — a plain
|
|
117
|
+
chain of concrete Key/Attr/Slot accesses with no patterns,
|
|
118
|
+
substitutions, references, guards, or filters — else None. Transforms
|
|
119
|
+
are permitted (they apply after the lookup). Computed once and
|
|
120
|
+
cached; drives the fast path in get() that skips walk().
|
|
120
121
|
kind is 'key', 'attr', or 'slot'.
|
|
121
122
|
"""
|
|
122
|
-
if self.
|
|
123
|
+
if self.guard is not None:
|
|
123
124
|
return None
|
|
124
125
|
chain = []
|
|
125
126
|
for op in self.ops:
|
|
@@ -72,6 +72,18 @@ class Wrap(base.TraversalOp):
|
|
|
72
72
|
except TypeError:
|
|
73
73
|
return self.inner.match(op)
|
|
74
74
|
|
|
75
|
+
def is_variadic(self):
|
|
76
|
+
return self.inner.is_variadic() if hasattr(self.inner, 'is_variadic') else False
|
|
77
|
+
|
|
78
|
+
def do_match(self, rest_pats, path_ops, partial):
|
|
79
|
+
"""
|
|
80
|
+
Variadic inners (recursive ops, groups) decide their own segment
|
|
81
|
+
consumption; otherwise match one segment via this wrap's match.
|
|
82
|
+
"""
|
|
83
|
+
if self.is_variadic():
|
|
84
|
+
return self.inner.do_match(rest_pats, path_ops, partial)
|
|
85
|
+
return super().do_match(rest_pats, path_ops, partial)
|
|
86
|
+
|
|
75
87
|
def concrete(self, val):
|
|
76
88
|
return self.inner.concrete(val)
|
|
77
89
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dotted_notation"
|
|
7
|
-
version = "0.44.
|
|
7
|
+
version = "0.44.6"
|
|
8
8
|
description = "Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.6"
|
|
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.44.4 → dotted_notation-0.44.6}/dotted_notation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|