dotted-notation 0.44.6__tar.gz → 0.44.8__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.6 → dotted_notation-0.44.8}/CHANGELOG.md +47 -0
- {dotted_notation-0.44.6/dotted_notation.egg-info → dotted_notation-0.44.8}/PKG-INFO +53 -1
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/README.md +52 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/api.py +38 -4
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/base.py +19 -5
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/groups.py +150 -4
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/recursive.py +50 -4
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/wrappers.py +15 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8/dotted_notation.egg-info}/PKG-INFO +53 -1
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/pyproject.toml +1 -1
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/LICENSE +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/MANIFEST.in +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/__init__.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/__main__.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/access.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/cli/main.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/containers.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/engine.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/filters.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/grammar.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/matchers.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/predicates.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/results.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/sql/core.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/sql/pg.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/transforms.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/utils.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted/utypes.py +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.44.6 → dotted_notation-0.44.8}/setup.cfg +0 -0
|
@@ -3,6 +3,53 @@
|
|
|
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.8]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- `match()` supports variadic ops on the *path* side. A group or
|
|
10
|
+
recursive op in the path used to fall through to `None` (or match by
|
|
11
|
+
accident, as `**` did); the path is now treated as the set of paths it
|
|
12
|
+
denotes, and the pattern must *subsume* it — cover every expansion.
|
|
13
|
+
Every branch of a path-side group must match, so
|
|
14
|
+
`match('references.*', 'references.(a,b)')` matches while
|
|
15
|
+
`match('*.*', '(a.b,c)')` does not (branch `c` is a segment short).
|
|
16
|
+
Groups on both sides compose the two rules: some pattern branch must
|
|
17
|
+
cover every path branch. A path-side recursive is covered only by a
|
|
18
|
+
recursive pattern that subsumes it — `match('**', '*b')` matches,
|
|
19
|
+
`match('*', '**')` does not. Conjunctions match on any branch (their
|
|
20
|
+
expansions are the intersection); negations, denoting an open set,
|
|
21
|
+
only by an identical negation or a bare wildcard.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- A path-side group captures as one segment: itself, e.g.
|
|
25
|
+
`match('references.*', 'references.(a,b)', groups=True)` gives
|
|
26
|
+
`('references.(a,b)', ('references', '(a,b)'))`. When a variadic
|
|
27
|
+
pattern consumes across the group boundary the group folds into that
|
|
28
|
+
segment's capture, as nested patterns already do.
|
|
29
|
+
- Match dispatch is now per-op on both sides: variadic path ops
|
|
30
|
+
implement `do_match_path` (the mirror of `do_match`), and segment
|
|
31
|
+
coverage moved onto ops as `covered_by`, replacing the ad-hoc value
|
|
32
|
+
extraction `Recursive.do_match` used to do.
|
|
33
|
+
|
|
34
|
+
## [0.44.7]
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- `groups='patterns'` capture positions for variadic patterns: literals
|
|
38
|
+
are now correctly excluded when the pattern contains a group or
|
|
39
|
+
recursive op, so `translate`'s `$N` numbering counts pattern segments
|
|
40
|
+
only, as documented. Previously `a.(x,y).*` numbered `$0='a'` (a
|
|
41
|
+
literal), and `**.c` captured the literal `c`.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
- A group is one pattern segment: it captures the path segments its
|
|
45
|
+
matching branch consumed as a single group, keeping `$N` positions
|
|
46
|
+
stable across branches of different lengths. `x.(a.b,c)` matching
|
|
47
|
+
`x.a.b` now captures `('x', 'a.b')` instead of `('x', 'a', 'b')`.
|
|
48
|
+
Parentheses thereby act as a regex-like capture group: `x.(a.b)`
|
|
49
|
+
captures `'a.b'` as one group where `x.a.b` captures `'a', 'b'`.
|
|
50
|
+
Nested patterns inside a branch fold into the group's capture, as
|
|
51
|
+
with `**`.
|
|
52
|
+
|
|
6
53
|
## [0.44.6]
|
|
7
54
|
|
|
8
55
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.44.
|
|
3
|
+
Version: 0.44.8
|
|
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: MIT
|
|
@@ -508,6 +508,58 @@ excluding literals:
|
|
|
508
508
|
>>> dotted.match('hello.*', 'hello.there.bye', groups=GroupMode.patterns)
|
|
509
509
|
('hello.there.bye', ('there.bye',))
|
|
510
510
|
|
|
511
|
+
Op groups match like regex alternation, and parentheses act as a capture
|
|
512
|
+
group: a group captures the segments its matching branch consumed as a
|
|
513
|
+
single group, and counts as one pattern position:
|
|
514
|
+
|
|
515
|
+
>>> dotted.match('a.(x,y).*', 'a.x.z', groups=GroupMode.patterns, partial=False)
|
|
516
|
+
('a.x.z', ('x', 'z'))
|
|
517
|
+
>>> dotted.match('x.(a.b,c)', 'x.a.b', groups=True)
|
|
518
|
+
('x.a.b', ('x', 'a.b'))
|
|
519
|
+
>>> dotted.match('x.(a.b)', 'x.a.b', groups=True)
|
|
520
|
+
('x.a.b', ('x', 'a.b'))
|
|
521
|
+
|
|
522
|
+
The path may itself be a pattern, in which case `match` asks whether the
|
|
523
|
+
pattern *subsumes* it — covers everything the path denotes:
|
|
524
|
+
|
|
525
|
+
>>> dotted.match('*', '*.*')
|
|
526
|
+
'*.*'
|
|
527
|
+
>>> dotted.match('*.*', '*')
|
|
528
|
+
|
|
529
|
+
A group on the path side denotes all of its expansions, so every branch
|
|
530
|
+
must be covered. It captures as a single segment: itself.
|
|
531
|
+
|
|
532
|
+
>>> dotted.match('references.*', 'references.(a,b)')
|
|
533
|
+
'references.(a,b)'
|
|
534
|
+
>>> dotted.match('references.*', 'references.(a,b)', groups=True)
|
|
535
|
+
('references.(a,b)', ('references', '(a,b)'))
|
|
536
|
+
|
|
537
|
+
A branch that outruns the pattern is only covered when partial matching
|
|
538
|
+
is on, and never when the pattern needs more segments than the branch
|
|
539
|
+
has:
|
|
540
|
+
|
|
541
|
+
>>> dotted.match('*', '(a.b,c)')
|
|
542
|
+
'(a.b,c)'
|
|
543
|
+
>>> dotted.match('*', '(a.b,c)', partial=False)
|
|
544
|
+
>>> dotted.match('*.*', '(a.b,c)')
|
|
545
|
+
|
|
546
|
+
Groups on both sides compose the two rules — the pattern matches if
|
|
547
|
+
*some* branch of it covers *every* branch of the path:
|
|
548
|
+
|
|
549
|
+
>>> dotted.match('(a,b)', '(a.b,b)')
|
|
550
|
+
'(a.b,b)'
|
|
551
|
+
>>> dotted.match('(a,b)', '(a.b,b)', partial=False)
|
|
552
|
+
>>> dotted.match('(a,b)', '(a.b,c)')
|
|
553
|
+
|
|
554
|
+
A recursive op on the path side denotes unboundedly many expansions, so
|
|
555
|
+
only a recursive pattern that subsumes it will match:
|
|
556
|
+
|
|
557
|
+
>>> dotted.match('**', 'a.**')
|
|
558
|
+
'a.**'
|
|
559
|
+
>>> dotted.match('**', '*b')
|
|
560
|
+
'*b'
|
|
561
|
+
>>> dotted.match('*', '**')
|
|
562
|
+
|
|
511
563
|
<a id="replace"></a>
|
|
512
564
|
### Replace
|
|
513
565
|
|
|
@@ -468,6 +468,58 @@ excluding literals:
|
|
|
468
468
|
>>> dotted.match('hello.*', 'hello.there.bye', groups=GroupMode.patterns)
|
|
469
469
|
('hello.there.bye', ('there.bye',))
|
|
470
470
|
|
|
471
|
+
Op groups match like regex alternation, and parentheses act as a capture
|
|
472
|
+
group: a group captures the segments its matching branch consumed as a
|
|
473
|
+
single group, and counts as one pattern position:
|
|
474
|
+
|
|
475
|
+
>>> dotted.match('a.(x,y).*', 'a.x.z', groups=GroupMode.patterns, partial=False)
|
|
476
|
+
('a.x.z', ('x', 'z'))
|
|
477
|
+
>>> dotted.match('x.(a.b,c)', 'x.a.b', groups=True)
|
|
478
|
+
('x.a.b', ('x', 'a.b'))
|
|
479
|
+
>>> dotted.match('x.(a.b)', 'x.a.b', groups=True)
|
|
480
|
+
('x.a.b', ('x', 'a.b'))
|
|
481
|
+
|
|
482
|
+
The path may itself be a pattern, in which case `match` asks whether the
|
|
483
|
+
pattern *subsumes* it — covers everything the path denotes:
|
|
484
|
+
|
|
485
|
+
>>> dotted.match('*', '*.*')
|
|
486
|
+
'*.*'
|
|
487
|
+
>>> dotted.match('*.*', '*')
|
|
488
|
+
|
|
489
|
+
A group on the path side denotes all of its expansions, so every branch
|
|
490
|
+
must be covered. It captures as a single segment: itself.
|
|
491
|
+
|
|
492
|
+
>>> dotted.match('references.*', 'references.(a,b)')
|
|
493
|
+
'references.(a,b)'
|
|
494
|
+
>>> dotted.match('references.*', 'references.(a,b)', groups=True)
|
|
495
|
+
('references.(a,b)', ('references', '(a,b)'))
|
|
496
|
+
|
|
497
|
+
A branch that outruns the pattern is only covered when partial matching
|
|
498
|
+
is on, and never when the pattern needs more segments than the branch
|
|
499
|
+
has:
|
|
500
|
+
|
|
501
|
+
>>> dotted.match('*', '(a.b,c)')
|
|
502
|
+
'(a.b,c)'
|
|
503
|
+
>>> dotted.match('*', '(a.b,c)', partial=False)
|
|
504
|
+
>>> dotted.match('*.*', '(a.b,c)')
|
|
505
|
+
|
|
506
|
+
Groups on both sides compose the two rules — the pattern matches if
|
|
507
|
+
*some* branch of it covers *every* branch of the path:
|
|
508
|
+
|
|
509
|
+
>>> dotted.match('(a,b)', '(a.b,b)')
|
|
510
|
+
'(a.b,b)'
|
|
511
|
+
>>> dotted.match('(a,b)', '(a.b,b)', partial=False)
|
|
512
|
+
>>> dotted.match('(a,b)', '(a.b,c)')
|
|
513
|
+
|
|
514
|
+
A recursive op on the path side denotes unboundedly many expansions, so
|
|
515
|
+
only a recursive pattern that subsumes it will match:
|
|
516
|
+
|
|
517
|
+
>>> dotted.match('**', 'a.**')
|
|
518
|
+
'a.**'
|
|
519
|
+
>>> dotted.match('**', '*b')
|
|
520
|
+
'*b'
|
|
521
|
+
>>> dotted.match('*', '**')
|
|
522
|
+
|
|
471
523
|
<a id="replace"></a>
|
|
472
524
|
### Replace
|
|
473
525
|
|
|
@@ -762,12 +762,46 @@ def match(pattern, path, groups=False, partial=True, strict=False):
|
|
|
762
762
|
'b'
|
|
763
763
|
>>> match('(!a)', 'a')
|
|
764
764
|
|
|
765
|
+
A group is one pattern segment: it captures the segments its branch
|
|
766
|
+
consumed as a single group, and counts as one pattern position:
|
|
767
|
+
>>> match('x.(a.b,c)', 'x.a.b', groups=True)
|
|
768
|
+
('x.a.b', ('x', 'a.b'))
|
|
769
|
+
>>> match('a.(x,y).*', 'a.x.z', groups='patterns', partial=False)
|
|
770
|
+
('a.x.z', ('x', 'z'))
|
|
771
|
+
|
|
765
772
|
Recursive patterns:
|
|
766
773
|
>>> match('**.c', 'a.b.c')
|
|
767
774
|
'a.b.c'
|
|
768
775
|
>>> match('*b', 'b.b.b')
|
|
769
776
|
'b.b.b'
|
|
770
777
|
>>> match('*b', 'a.b.c')
|
|
778
|
+
|
|
779
|
+
A group on the *path* side denotes all its expansions; the pattern
|
|
780
|
+
must cover every branch (subsumption). The group captures as one
|
|
781
|
+
segment: itself.
|
|
782
|
+
>>> match('references.*', 'references.(a,b)')
|
|
783
|
+
'references.(a,b)'
|
|
784
|
+
>>> match('references.*', 'references.(a,b)', groups=True)
|
|
785
|
+
('references.(a,b)', ('references', '(a,b)'))
|
|
786
|
+
>>> match('**.z', '(a.b,c).z', groups=True)
|
|
787
|
+
('(a.b,c).z', ('(a.b,c)', 'z'))
|
|
788
|
+
>>> match('*', '(a.b,c)')
|
|
789
|
+
'(a.b,c)'
|
|
790
|
+
>>> match('*', '(a.b,c)', partial=False)
|
|
791
|
+
>>> match('*.*', '(a.b,c)')
|
|
792
|
+
>>> match('(a,b)', '(a.b,c)')
|
|
793
|
+
>>> match('(a,b)', '(a.b,b)')
|
|
794
|
+
'(a.b,b)'
|
|
795
|
+
>>> match('**.z', '(a.b,c).z')
|
|
796
|
+
'(a.b,c).z'
|
|
797
|
+
|
|
798
|
+
A recursive op on the path side is covered only by a recursive
|
|
799
|
+
pattern that subsumes it:
|
|
800
|
+
>>> match('**', 'a.**')
|
|
801
|
+
'a.**'
|
|
802
|
+
>>> match('**', '*b')
|
|
803
|
+
'*b'
|
|
804
|
+
>>> match('*', '**')
|
|
771
805
|
"""
|
|
772
806
|
# groups can be a bool, a GroupMode member, or its string value.
|
|
773
807
|
_patterns_only = (groups == GroupMode.patterns
|
|
@@ -784,13 +818,13 @@ def match(pattern, path, groups=False, partial=True, strict=False):
|
|
|
784
818
|
path_ops = parse(path)
|
|
785
819
|
|
|
786
820
|
# Variadic ops (recursive, groups) consume variable-length path
|
|
787
|
-
# segments — use the recursive matcher
|
|
788
|
-
|
|
821
|
+
# segments — use the recursive matcher. On the path side they make
|
|
822
|
+
# the path denote multiple expansions, which likewise needs it.
|
|
823
|
+
if any(op.is_variadic() for op in pats) or any(op.is_variadic() for op in path_ops):
|
|
789
824
|
result = base.match_ops(list(pats), list(path_ops), partial)
|
|
790
825
|
if result is None:
|
|
791
826
|
return returns(None, [])
|
|
792
|
-
|
|
793
|
-
return returns(path, result)
|
|
827
|
+
return returns(path, [v for v, _ in result], [p for _, p in result])
|
|
794
828
|
|
|
795
829
|
# Original non-recursive match logic
|
|
796
830
|
_matches = []
|
|
@@ -45,10 +45,14 @@ def has_any(gen):
|
|
|
45
45
|
def match_ops(pats, path_ops, partial):
|
|
46
46
|
"""
|
|
47
47
|
Match a list of pattern ops against a list of path ops.
|
|
48
|
-
Returns a list of
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
Returns a list of (value, is_pattern) capture pairs on success, None
|
|
49
|
+
on failure. Dispatch is per-op on both sides: a variadic op at the
|
|
50
|
+
head of the path (group, recursive) decides how the pattern must
|
|
51
|
+
cover it via do_match_path; otherwise the head pattern op decides
|
|
52
|
+
how many path segments it consumes via do_match.
|
|
51
53
|
"""
|
|
54
|
+
if path_ops and path_ops[0].is_variadic():
|
|
55
|
+
return path_ops[0].do_match_path(list(pats), list(path_ops[1:]), partial)
|
|
52
56
|
if pats:
|
|
53
57
|
return pats[0].do_match(pats[1:], path_ops, partial)
|
|
54
58
|
if not path_ops:
|
|
@@ -207,6 +211,15 @@ class TraversalOp(Op):
|
|
|
207
211
|
"""
|
|
208
212
|
return False
|
|
209
213
|
|
|
214
|
+
def covered_by(self, matcher):
|
|
215
|
+
"""
|
|
216
|
+
True if *matcher* (a match op) matches every segment this op
|
|
217
|
+
denotes when it appears on the path side. Simple ops denote a
|
|
218
|
+
single concrete segment; variadic ops override.
|
|
219
|
+
"""
|
|
220
|
+
val = getattr(getattr(self, 'op', self), 'value', self)
|
|
221
|
+
return has_any(matcher.matches((val,)))
|
|
222
|
+
|
|
210
223
|
def do_match(self, rest_pats, path_ops, partial):
|
|
211
224
|
"""
|
|
212
225
|
Match this op against exactly one path segment, then continue with
|
|
@@ -221,9 +234,10 @@ class TraversalOp(Op):
|
|
|
221
234
|
rest = match_ops(rest_pats, path_ops[1:], partial)
|
|
222
235
|
if rest is None:
|
|
223
236
|
return None
|
|
237
|
+
is_pat = self.is_pattern()
|
|
224
238
|
if isinstance(m, (tuple, list)):
|
|
225
|
-
return [_m.val for _m in m] + rest
|
|
226
|
-
return [m.val] + rest
|
|
239
|
+
return [(_m.val, is_pat) for _m in m] + rest
|
|
240
|
+
return [(m.val, is_pat)] + rest
|
|
227
241
|
|
|
228
242
|
|
|
229
243
|
class MatchOp(Op):
|
|
@@ -109,13 +109,104 @@ class OpGroup(base.TraversalOp):
|
|
|
109
109
|
concrete path can only be produced by one branch at a time, so
|
|
110
110
|
disjunction, first-match, and conjunction all reduce to "any branch
|
|
111
111
|
matches"; cut markers don't constrain matching either.
|
|
112
|
+
|
|
113
|
+
The group is one pattern segment: it contributes a single capture —
|
|
114
|
+
the path segments its branch consumed, assembled — flagged as a
|
|
115
|
+
pattern match (like Recursive).
|
|
116
|
+
"""
|
|
117
|
+
from . import results
|
|
118
|
+
for branch in base.branches_only(self.branches):
|
|
119
|
+
branch_ops = list(branch)
|
|
120
|
+
for n in range(len(path_ops) + 1):
|
|
121
|
+
consumed = base.match_ops(branch_ops, path_ops[:n], False)
|
|
122
|
+
if consumed is None:
|
|
123
|
+
continue
|
|
124
|
+
rest = base.match_ops(list(rest_pats), path_ops[n:], partial)
|
|
125
|
+
if rest is None:
|
|
126
|
+
continue
|
|
127
|
+
combined = results.assemble(path_ops[:n])
|
|
128
|
+
return [(combined, True)] + rest
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def do_match_path(self, pats, rest_path, partial):
|
|
112
132
|
"""
|
|
133
|
+
Match when this group appears on the *path* side. The path
|
|
134
|
+
denotes every expansion of the group's branches, so the pattern
|
|
135
|
+
must cover all of them (subsumption): each branch is spliced
|
|
136
|
+
into the path in the group's place and matched; one failing
|
|
137
|
+
branch fails the whole match.
|
|
138
|
+
|
|
139
|
+
The group captures as itself — one capture — via an aligned
|
|
140
|
+
decomposition (a pattern prefix covering every branch), or
|
|
141
|
+
assembled into a variadic pattern segment's capture when the
|
|
142
|
+
pattern crosses the group boundary; only when neither
|
|
143
|
+
decomposition exists do captures fall back to the first
|
|
144
|
+
branch's expansion.
|
|
145
|
+
"""
|
|
146
|
+
fallback = self._verify_covered(pats, rest_path, partial)
|
|
147
|
+
if fallback is None:
|
|
148
|
+
return None
|
|
149
|
+
r = self._aligned_captures(pats, rest_path, partial)
|
|
150
|
+
if r is not None:
|
|
151
|
+
return r
|
|
152
|
+
if pats and pats[0].is_variadic():
|
|
153
|
+
r = pats[0].do_match(pats[1:], [self] + list(rest_path), partial)
|
|
154
|
+
if r is not None:
|
|
155
|
+
return r
|
|
156
|
+
return fallback
|
|
157
|
+
|
|
158
|
+
def _verify_covered(self, pats, rest_path, partial):
|
|
159
|
+
"""
|
|
160
|
+
Subsumption check over expansions: splice each branch into the
|
|
161
|
+
path and match. Returns the first branch's result (the capture
|
|
162
|
+
fallback) or None when any branch is uncovered.
|
|
163
|
+
"""
|
|
164
|
+
first = base.marker
|
|
113
165
|
for branch in base.branches_only(self.branches):
|
|
114
|
-
|
|
115
|
-
if
|
|
116
|
-
return
|
|
166
|
+
r = base.match_ops(pats, list(branch) + list(rest_path), partial)
|
|
167
|
+
if r is None:
|
|
168
|
+
return None
|
|
169
|
+
if first is base.marker:
|
|
170
|
+
first = r
|
|
171
|
+
if first is base.marker:
|
|
172
|
+
return base.match_ops(pats, list(rest_path), partial)
|
|
173
|
+
return first
|
|
174
|
+
|
|
175
|
+
def _aligned_captures(self, pats, rest_path, partial):
|
|
176
|
+
"""
|
|
177
|
+
Group-as-itself captures: find a pattern prefix that covers the
|
|
178
|
+
branch set exactly (or up to a partial tail when the pattern
|
|
179
|
+
ends inside the group); the group then captures as one segment
|
|
180
|
+
and the remaining pattern matches the remaining path.
|
|
181
|
+
"""
|
|
182
|
+
from . import results
|
|
183
|
+
for k in range(1, len(pats) + 1):
|
|
184
|
+
head = list(pats[:k])
|
|
185
|
+
tail_partial = partial and k == len(pats) and not rest_path
|
|
186
|
+
if not self._covers_branches(head, tail_partial):
|
|
187
|
+
continue
|
|
188
|
+
rest = base.match_ops(list(pats[k:]), list(rest_path), partial)
|
|
189
|
+
if rest is None:
|
|
190
|
+
continue
|
|
191
|
+
is_pat = any(p.is_pattern() for p in head)
|
|
192
|
+
return [(results.assemble([self]), is_pat)] + rest
|
|
117
193
|
return None
|
|
118
194
|
|
|
195
|
+
def _covers_branches(self, head_pats, tail_partial=False):
|
|
196
|
+
"""
|
|
197
|
+
True if head_pats covers every branch of this group.
|
|
198
|
+
"""
|
|
199
|
+
return all(base.match_ops(head_pats, list(b), tail_partial) is not None
|
|
200
|
+
for b in base.branches_only(self.branches))
|
|
201
|
+
|
|
202
|
+
def covered_by(self, matcher):
|
|
203
|
+
"""
|
|
204
|
+
A group path op is covered when every segment of every branch is.
|
|
205
|
+
"""
|
|
206
|
+
return all(op.covered_by(matcher)
|
|
207
|
+
for branch in base.branches_only(self.branches)
|
|
208
|
+
for op in branch)
|
|
209
|
+
|
|
119
210
|
def _render(self, top=True):
|
|
120
211
|
"""
|
|
121
212
|
Render the group as a string. Subclasses override this.
|
|
@@ -417,6 +508,25 @@ class OpGroupAnd(OpGroup):
|
|
|
417
508
|
def __repr__(self):
|
|
418
509
|
return self._render(top=True)
|
|
419
510
|
|
|
511
|
+
def _verify_covered(self, pats, rest_path, partial):
|
|
512
|
+
"""
|
|
513
|
+
A conjunction's expansions are the intersection of its
|
|
514
|
+
branches', so a pattern covering any single branch covers the
|
|
515
|
+
whole (sufficient, conservatively incomplete).
|
|
516
|
+
"""
|
|
517
|
+
for branch in base.branches_only(self.branches):
|
|
518
|
+
r = base.match_ops(pats, list(branch) + list(rest_path), partial)
|
|
519
|
+
if r is not None:
|
|
520
|
+
return r
|
|
521
|
+
return None
|
|
522
|
+
|
|
523
|
+
def _covers_branches(self, head_pats, tail_partial=False):
|
|
524
|
+
"""
|
|
525
|
+
Any single covered branch suffices for a conjunction.
|
|
526
|
+
"""
|
|
527
|
+
return any(base.match_ops(head_pats, list(b), tail_partial) is not None
|
|
528
|
+
for b in base.branches_only(self.branches))
|
|
529
|
+
|
|
420
530
|
def push_children(self, stack, frame, paths):
|
|
421
531
|
"""
|
|
422
532
|
All branches must match. Collect results per branch;
|
|
@@ -552,7 +662,43 @@ class OpGroupNot(OpGroup):
|
|
|
552
662
|
if rest is None:
|
|
553
663
|
return None
|
|
554
664
|
seg_val = getattr(getattr(kop, 'op', kop), 'value', kop)
|
|
555
|
-
return [seg_val] + rest
|
|
665
|
+
return [(seg_val, True)] + rest
|
|
666
|
+
|
|
667
|
+
def do_match_path(self, pats, rest_path, partial):
|
|
668
|
+
"""
|
|
669
|
+
Match when this negation appears on the *path* side. It denotes
|
|
670
|
+
every segment its inner pattern excludes — an open set — so it
|
|
671
|
+
is covered only by an identical negation or by a bare wildcard
|
|
672
|
+
segment.
|
|
673
|
+
"""
|
|
674
|
+
from . import results
|
|
675
|
+
if not pats:
|
|
676
|
+
return [] if partial else None
|
|
677
|
+
head = pats[0]
|
|
678
|
+
if head != self and not self._wildcard_covers(head):
|
|
679
|
+
return None
|
|
680
|
+
rest = base.match_ops(pats[1:], rest_path, partial)
|
|
681
|
+
if rest is None:
|
|
682
|
+
return None
|
|
683
|
+
return [(results.assemble([self]), True)] + rest
|
|
684
|
+
|
|
685
|
+
def _wildcard_covers(self, head):
|
|
686
|
+
"""
|
|
687
|
+
True if pattern op *head* is a bare single-segment wildcard and
|
|
688
|
+
this negation excludes only single segments.
|
|
689
|
+
"""
|
|
690
|
+
from . import matchers
|
|
691
|
+
if any(len(b) != 1 for b in base.branches_only(self.branches)):
|
|
692
|
+
return False
|
|
693
|
+
return isinstance(getattr(head, 'op', None), matchers.Wildcard)
|
|
694
|
+
|
|
695
|
+
def covered_by(self, matcher):
|
|
696
|
+
"""
|
|
697
|
+
A negation denotes an open set of segments; only a wildcard
|
|
698
|
+
covers them all.
|
|
699
|
+
"""
|
|
700
|
+
from . import matchers
|
|
701
|
+
return isinstance(matcher, matchers.Wildcard)
|
|
556
702
|
|
|
557
703
|
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, **kwargs):
|
|
558
704
|
inner = self.inner
|
|
@@ -125,17 +125,63 @@ class Recursive(BaseOp):
|
|
|
125
125
|
"""
|
|
126
126
|
from . import results
|
|
127
127
|
for n in range(1, len(path_ops) + 1):
|
|
128
|
-
|
|
129
|
-
seg_val = getattr(getattr(kop, 'op', kop), 'value', kop)
|
|
130
|
-
if not any(True for _ in self.inner.matches((seg_val,))):
|
|
128
|
+
if not path_ops[n - 1].covered_by(self.inner):
|
|
131
129
|
return None
|
|
132
130
|
rest = base.match_ops(rest_pats, path_ops[n:], partial)
|
|
133
131
|
if rest is None:
|
|
134
132
|
continue
|
|
135
133
|
combined = results.assemble(path_ops[:n])
|
|
136
|
-
return [combined] + rest
|
|
134
|
+
return [(combined, True)] + rest
|
|
137
135
|
return None
|
|
138
136
|
|
|
137
|
+
def do_match_path(self, pats, rest_path, partial):
|
|
138
|
+
"""
|
|
139
|
+
Match when this recursive op appears on the *path* side. It
|
|
140
|
+
denotes unboundedly many expansions, so only a pattern segment
|
|
141
|
+
that itself covers arbitrary depth can subsume it: a recursive
|
|
142
|
+
whose inner pattern covers this op's chain segments. The
|
|
143
|
+
subsuming pattern op is tried both consumed and retained, since
|
|
144
|
+
a recursive pattern may keep covering segments after the group.
|
|
145
|
+
"""
|
|
146
|
+
from . import results
|
|
147
|
+
if not pats:
|
|
148
|
+
return [] if partial else None
|
|
149
|
+
head = pats[0]
|
|
150
|
+
if not isinstance(head, Recursive):
|
|
151
|
+
return None
|
|
152
|
+
if not self._subsumed_by(head):
|
|
153
|
+
return None
|
|
154
|
+
rest = base.match_ops(pats[1:], rest_path, partial)
|
|
155
|
+
if rest is None:
|
|
156
|
+
rest = base.match_ops(pats, rest_path, partial)
|
|
157
|
+
if rest is None:
|
|
158
|
+
return None
|
|
159
|
+
return [(results.assemble([self]), True)] + rest
|
|
160
|
+
|
|
161
|
+
def _subsumed_by(self, pat):
|
|
162
|
+
"""
|
|
163
|
+
True if recursive pattern op *pat* covers every expansion of
|
|
164
|
+
this path op. Identical recursives cover; otherwise an
|
|
165
|
+
unsliced recursive with the same accessors whose inner matcher
|
|
166
|
+
covers every chain segment this op can produce.
|
|
167
|
+
"""
|
|
168
|
+
if pat == self:
|
|
169
|
+
return True
|
|
170
|
+
if pat.depth_start is not None or pat.depth_stop is not None or pat.depth_step is not None:
|
|
171
|
+
return False
|
|
172
|
+
if pat.accessors != self.accessors:
|
|
173
|
+
return False
|
|
174
|
+
return self.covered_by(pat.inner)
|
|
175
|
+
|
|
176
|
+
def covered_by(self, matcher):
|
|
177
|
+
"""
|
|
178
|
+
A recursive path op denotes unboundedly many segments; only a
|
|
179
|
+
wildcard, or this op's own inner matcher, covers them all.
|
|
180
|
+
"""
|
|
181
|
+
if isinstance(matcher, matchers.Wildcard):
|
|
182
|
+
return True
|
|
183
|
+
return matcher == self.inner
|
|
184
|
+
|
|
139
185
|
def _effective_branches(self):
|
|
140
186
|
"""
|
|
141
187
|
Return accessor branches that drive recursion.
|
|
@@ -84,6 +84,21 @@ class Wrap(base.TraversalOp):
|
|
|
84
84
|
return self.inner.do_match(rest_pats, path_ops, partial)
|
|
85
85
|
return super().do_match(rest_pats, path_ops, partial)
|
|
86
86
|
|
|
87
|
+
def do_match_path(self, pats, rest_path, partial):
|
|
88
|
+
"""
|
|
89
|
+
Delegate path-side matching to the wrapped op (only reached for
|
|
90
|
+
variadic inners, via is_variadic).
|
|
91
|
+
"""
|
|
92
|
+
return self.inner.do_match_path(pats, rest_path, partial)
|
|
93
|
+
|
|
94
|
+
def covered_by(self, matcher):
|
|
95
|
+
"""
|
|
96
|
+
Delegate segment coverage to the wrapped op.
|
|
97
|
+
"""
|
|
98
|
+
if hasattr(self.inner, 'covered_by'):
|
|
99
|
+
return self.inner.covered_by(matcher)
|
|
100
|
+
return super().covered_by(matcher)
|
|
101
|
+
|
|
87
102
|
def concrete(self, val):
|
|
88
103
|
return self.inner.concrete(val)
|
|
89
104
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.44.
|
|
3
|
+
Version: 0.44.8
|
|
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: MIT
|
|
@@ -508,6 +508,58 @@ excluding literals:
|
|
|
508
508
|
>>> dotted.match('hello.*', 'hello.there.bye', groups=GroupMode.patterns)
|
|
509
509
|
('hello.there.bye', ('there.bye',))
|
|
510
510
|
|
|
511
|
+
Op groups match like regex alternation, and parentheses act as a capture
|
|
512
|
+
group: a group captures the segments its matching branch consumed as a
|
|
513
|
+
single group, and counts as one pattern position:
|
|
514
|
+
|
|
515
|
+
>>> dotted.match('a.(x,y).*', 'a.x.z', groups=GroupMode.patterns, partial=False)
|
|
516
|
+
('a.x.z', ('x', 'z'))
|
|
517
|
+
>>> dotted.match('x.(a.b,c)', 'x.a.b', groups=True)
|
|
518
|
+
('x.a.b', ('x', 'a.b'))
|
|
519
|
+
>>> dotted.match('x.(a.b)', 'x.a.b', groups=True)
|
|
520
|
+
('x.a.b', ('x', 'a.b'))
|
|
521
|
+
|
|
522
|
+
The path may itself be a pattern, in which case `match` asks whether the
|
|
523
|
+
pattern *subsumes* it — covers everything the path denotes:
|
|
524
|
+
|
|
525
|
+
>>> dotted.match('*', '*.*')
|
|
526
|
+
'*.*'
|
|
527
|
+
>>> dotted.match('*.*', '*')
|
|
528
|
+
|
|
529
|
+
A group on the path side denotes all of its expansions, so every branch
|
|
530
|
+
must be covered. It captures as a single segment: itself.
|
|
531
|
+
|
|
532
|
+
>>> dotted.match('references.*', 'references.(a,b)')
|
|
533
|
+
'references.(a,b)'
|
|
534
|
+
>>> dotted.match('references.*', 'references.(a,b)', groups=True)
|
|
535
|
+
('references.(a,b)', ('references', '(a,b)'))
|
|
536
|
+
|
|
537
|
+
A branch that outruns the pattern is only covered when partial matching
|
|
538
|
+
is on, and never when the pattern needs more segments than the branch
|
|
539
|
+
has:
|
|
540
|
+
|
|
541
|
+
>>> dotted.match('*', '(a.b,c)')
|
|
542
|
+
'(a.b,c)'
|
|
543
|
+
>>> dotted.match('*', '(a.b,c)', partial=False)
|
|
544
|
+
>>> dotted.match('*.*', '(a.b,c)')
|
|
545
|
+
|
|
546
|
+
Groups on both sides compose the two rules — the pattern matches if
|
|
547
|
+
*some* branch of it covers *every* branch of the path:
|
|
548
|
+
|
|
549
|
+
>>> dotted.match('(a,b)', '(a.b,b)')
|
|
550
|
+
'(a.b,b)'
|
|
551
|
+
>>> dotted.match('(a,b)', '(a.b,b)', partial=False)
|
|
552
|
+
>>> dotted.match('(a,b)', '(a.b,c)')
|
|
553
|
+
|
|
554
|
+
A recursive op on the path side denotes unboundedly many expansions, so
|
|
555
|
+
only a recursive pattern that subsumes it will match:
|
|
556
|
+
|
|
557
|
+
>>> dotted.match('**', 'a.**')
|
|
558
|
+
'a.**'
|
|
559
|
+
>>> dotted.match('**', '*b')
|
|
560
|
+
'*b'
|
|
561
|
+
>>> dotted.match('*', '**')
|
|
562
|
+
|
|
511
563
|
<a id="replace"></a>
|
|
512
564
|
### Replace
|
|
513
565
|
|
|
@@ -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.8"
|
|
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.6 → dotted_notation-0.44.8}/dotted_notation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|