dotted-notation 0.44.5__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.5 → dotted_notation-0.44.6}/CHANGELOG.md +24 -0
- {dotted_notation-0.44.5/dotted_notation.egg-info → dotted_notation-0.44.6}/PKG-INFO +1 -1
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/api.py +21 -51
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/base.py +40 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/groups.py +36 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/recursive.py +22 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/wrappers.py +12 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6/dotted_notation.egg-info}/PKG-INFO +1 -1
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/pyproject.toml +1 -1
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/LICENSE +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/MANIFEST.in +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/README.md +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/__init__.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/__main__.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/access.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/cli/main.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/containers.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/engine.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/filters.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/grammar.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/matchers.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/predicates.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/results.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/sql/core.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/sql/pg.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/transforms.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/utils.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/utypes.py +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.44.5 → dotted_notation-0.44.6}/setup.cfg +0 -0
|
@@ -3,6 +3,30 @@
|
|
|
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
|
+
|
|
6
30
|
## [0.44.5]
|
|
7
31
|
|
|
8
32
|
### Performance
|
|
@@ -722,51 +722,6 @@ def remove_multi(obj, iterable, paths_only=True, mutable=True, strict=False, bin
|
|
|
722
722
|
return remove_if_multi(obj, iterable, paths_only=False, pred=None, mutable=mutable, strict=strict, bindings=bindings)
|
|
723
723
|
|
|
724
724
|
|
|
725
|
-
def _match_ops(pats, path_ops, partial):
|
|
726
|
-
"""
|
|
727
|
-
Recursive match of pattern ops against path ops.
|
|
728
|
-
Returns list of match values on success, None on failure.
|
|
729
|
-
Handles Recursive ops which can consume variable-length path segments.
|
|
730
|
-
"""
|
|
731
|
-
if not pats:
|
|
732
|
-
if not path_ops:
|
|
733
|
-
return []
|
|
734
|
-
if partial:
|
|
735
|
-
return []
|
|
736
|
-
return None
|
|
737
|
-
|
|
738
|
-
pop = pats[0]
|
|
739
|
-
rest_pats = pats[1:]
|
|
740
|
-
|
|
741
|
-
# Non-recursive op: consume exactly one path segment
|
|
742
|
-
if not pop.is_recursive():
|
|
743
|
-
if not path_ops:
|
|
744
|
-
return None
|
|
745
|
-
kop = path_ops[0]
|
|
746
|
-
m = pop.match(kop, specials=True)
|
|
747
|
-
if not m:
|
|
748
|
-
return None
|
|
749
|
-
rest_result = _match_ops(rest_pats, path_ops[1:], partial)
|
|
750
|
-
if rest_result is None:
|
|
751
|
-
return None
|
|
752
|
-
if isinstance(m, (tuple, list)):
|
|
753
|
-
return [_m.val for _m in m] + rest_result
|
|
754
|
-
return [m.val] + rest_result
|
|
755
|
-
|
|
756
|
-
# Recursive op: try consuming 1, 2, ... N path segments via backtracking
|
|
757
|
-
for n in range(1, len(path_ops) + 1):
|
|
758
|
-
kop = path_ops[n - 1]
|
|
759
|
-
seg_val = getattr(getattr(kop, 'op', kop), 'value', kop)
|
|
760
|
-
matched = any(True for _ in pop.inner.matches((seg_val,)))
|
|
761
|
-
if not matched:
|
|
762
|
-
break # chain-following: stop extending once a segment fails
|
|
763
|
-
rest_result = _match_ops(rest_pats, path_ops[n:], partial)
|
|
764
|
-
if rest_result is not None:
|
|
765
|
-
combined = results.assemble(path_ops[:n])
|
|
766
|
-
return [combined] + rest_result
|
|
767
|
-
return None
|
|
768
|
-
|
|
769
|
-
|
|
770
725
|
def match(pattern, path, groups=False, partial=True, strict=False):
|
|
771
726
|
"""
|
|
772
727
|
Returns `path` if `pattern` matches; otherwise `None`
|
|
@@ -791,6 +746,22 @@ def match(pattern, path, groups=False, partial=True, strict=False):
|
|
|
791
746
|
>>> match('hello.*', 'hello.there.bye', groups='patterns')
|
|
792
747
|
('hello.there.bye', ('there.bye',))
|
|
793
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
|
+
|
|
794
765
|
Recursive patterns:
|
|
795
766
|
>>> match('**.c', 'a.b.c')
|
|
796
767
|
'a.b.c'
|
|
@@ -812,14 +783,13 @@ def match(pattern, path, groups=False, partial=True, strict=False):
|
|
|
812
783
|
pats = parse(pattern)
|
|
813
784
|
path_ops = parse(path)
|
|
814
785
|
|
|
815
|
-
#
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
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)
|
|
820
790
|
if result is None:
|
|
821
791
|
return returns(None, [])
|
|
822
|
-
# TODO: pattern-only filtering for
|
|
792
|
+
# TODO: pattern-only filtering for variadic matches
|
|
823
793
|
return returns(path, result)
|
|
824
794
|
|
|
825
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.
|
|
@@ -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
|
|
File without changes
|
{dotted_notation-0.44.5 → 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
|