dotted-notation 0.44.7__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.
Files changed (38) hide show
  1. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/CHANGELOG.md +28 -0
  2. {dotted_notation-0.44.7/dotted_notation.egg-info → dotted_notation-0.44.8}/PKG-INFO +42 -1
  3. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/README.md +41 -0
  4. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/api.py +30 -2
  5. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/base.py +15 -2
  6. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/groups.py +134 -0
  7. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/recursive.py +49 -3
  8. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/wrappers.py +15 -0
  9. {dotted_notation-0.44.7 → dotted_notation-0.44.8/dotted_notation.egg-info}/PKG-INFO +42 -1
  10. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/pyproject.toml +1 -1
  11. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/LICENSE +0 -0
  12. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/MANIFEST.in +0 -0
  13. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/__init__.py +0 -0
  14. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/__main__.py +0 -0
  15. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/access.py +0 -0
  16. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/cli/__init__.py +0 -0
  17. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/cli/_compat.py +0 -0
  18. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/cli/formats.py +0 -0
  19. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/cli/main.py +0 -0
  20. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/containers.py +0 -0
  21. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/engine.py +0 -0
  22. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/filters.py +0 -0
  23. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/grammar.py +0 -0
  24. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/matchers.py +0 -0
  25. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/predicates.py +0 -0
  26. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/results.py +0 -0
  27. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/sql/__init__.py +0 -0
  28. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/sql/core.py +0 -0
  29. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/sql/pg.py +0 -0
  30. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/transforms.py +0 -0
  31. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/utils.py +0 -0
  32. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted/utypes.py +0 -0
  33. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted_notation.egg-info/SOURCES.txt +0 -0
  34. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted_notation.egg-info/dependency_links.txt +0 -0
  35. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted_notation.egg-info/entry_points.txt +0 -0
  36. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted_notation.egg-info/requires.txt +0 -0
  37. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/dotted_notation.egg-info/top_level.txt +0 -0
  38. {dotted_notation-0.44.7 → dotted_notation-0.44.8}/setup.cfg +0 -0
@@ -3,6 +3,34 @@
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
+
6
34
  ## [0.44.7]
7
35
 
8
36
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.44.7
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
@@ -519,6 +519,47 @@ single group, and counts as one pattern position:
519
519
  >>> dotted.match('x.(a.b)', 'x.a.b', groups=True)
520
520
  ('x.a.b', ('x', 'a.b'))
521
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
+
522
563
  <a id="replace"></a>
523
564
  ### Replace
524
565
 
@@ -479,6 +479,47 @@ single group, and counts as one pattern position:
479
479
  >>> dotted.match('x.(a.b)', 'x.a.b', groups=True)
480
480
  ('x.a.b', ('x', 'a.b'))
481
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
+
482
523
  <a id="replace"></a>
483
524
  ### Replace
484
525
 
@@ -775,6 +775,33 @@ def match(pattern, path, groups=False, partial=True, strict=False):
775
775
  >>> match('*b', 'b.b.b')
776
776
  'b.b.b'
777
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('*', '**')
778
805
  """
779
806
  # groups can be a bool, a GroupMode member, or its string value.
780
807
  _patterns_only = (groups == GroupMode.patterns
@@ -791,8 +818,9 @@ def match(pattern, path, groups=False, partial=True, strict=False):
791
818
  path_ops = parse(path)
792
819
 
793
820
  # Variadic ops (recursive, groups) consume variable-length path
794
- # segments — use the recursive matcher
795
- if any(op.is_variadic() for op in pats):
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):
796
824
  result = base.match_ops(list(pats), list(path_ops), partial)
797
825
  if result is None:
798
826
  return returns(None, [])
@@ -46,9 +46,13 @@ def match_ops(pats, path_ops, partial):
46
46
  """
47
47
  Match a list of pattern ops against a list of path ops.
48
48
  Returns a list of (value, is_pattern) capture pairs on success, None
49
- on failure. Dispatches to each op's do_match, which decides how
50
- many path segments it consumes.
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
@@ -128,6 +128,85 @@ class OpGroup(base.TraversalOp):
128
128
  return [(combined, True)] + rest
129
129
  return None
130
130
 
131
+ def do_match_path(self, pats, rest_path, partial):
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
165
+ for branch in base.branches_only(self.branches):
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
193
+ return None
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
+
131
210
  def _render(self, top=True):
132
211
  """
133
212
  Render the group as a string. Subclasses override this.
@@ -429,6 +508,25 @@ class OpGroupAnd(OpGroup):
429
508
  def __repr__(self):
430
509
  return self._render(top=True)
431
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
+
432
530
  def push_children(self, stack, frame, paths):
433
531
  """
434
532
  All branches must match. Collect results per branch;
@@ -566,6 +664,42 @@ class OpGroupNot(OpGroup):
566
664
  seg_val = getattr(getattr(kop, 'op', kop), 'value', kop)
567
665
  return [(seg_val, True)] + rest
568
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)
702
+
569
703
  def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, **kwargs):
570
704
  inner = self.inner
571
705
  if not inner:
@@ -125,9 +125,7 @@ class Recursive(BaseOp):
125
125
  """
126
126
  from . import results
127
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,))):
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:
@@ -136,6 +134,54 @@ class Recursive(BaseOp):
136
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.7
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
@@ -519,6 +519,47 @@ single group, and counts as one pattern position:
519
519
  >>> dotted.match('x.(a.b)', 'x.a.b', groups=True)
520
520
  ('x.a.b', ('x', 'a.b'))
521
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
+
522
563
  <a id="replace"></a>
523
564
  ### Replace
524
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"
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"