dotted-notation 0.44.5__tar.gz → 0.44.7__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.5 → dotted_notation-0.44.7}/CHANGELOG.md +43 -0
  2. {dotted_notation-0.44.5/dotted_notation.egg-info → dotted_notation-0.44.7}/PKG-INFO +12 -1
  3. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/README.md +11 -0
  4. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/api.py +28 -52
  5. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/base.py +41 -0
  6. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/groups.py +48 -0
  7. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/recursive.py +22 -0
  8. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/wrappers.py +12 -0
  9. {dotted_notation-0.44.5 → dotted_notation-0.44.7/dotted_notation.egg-info}/PKG-INFO +12 -1
  10. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/pyproject.toml +1 -1
  11. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/LICENSE +0 -0
  12. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/MANIFEST.in +0 -0
  13. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/__init__.py +0 -0
  14. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/__main__.py +0 -0
  15. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/access.py +0 -0
  16. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/cli/__init__.py +0 -0
  17. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/cli/_compat.py +0 -0
  18. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/cli/formats.py +0 -0
  19. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/cli/main.py +0 -0
  20. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/containers.py +0 -0
  21. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/engine.py +0 -0
  22. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/filters.py +0 -0
  23. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/grammar.py +0 -0
  24. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/matchers.py +0 -0
  25. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/predicates.py +0 -0
  26. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/results.py +0 -0
  27. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/sql/__init__.py +0 -0
  28. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/sql/core.py +0 -0
  29. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/sql/pg.py +0 -0
  30. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/transforms.py +0 -0
  31. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/utils.py +0 -0
  32. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted/utypes.py +0 -0
  33. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted_notation.egg-info/SOURCES.txt +0 -0
  34. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted_notation.egg-info/dependency_links.txt +0 -0
  35. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted_notation.egg-info/entry_points.txt +0 -0
  36. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted_notation.egg-info/requires.txt +0 -0
  37. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/dotted_notation.egg-info/top_level.txt +0 -0
  38. {dotted_notation-0.44.5 → dotted_notation-0.44.7}/setup.cfg +0 -0
@@ -3,6 +3,49 @@
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.7]
7
+
8
+ ### Fixed
9
+ - `groups='patterns'` capture positions for variadic patterns: literals
10
+ are now correctly excluded when the pattern contains a group or
11
+ recursive op, so `translate`'s `$N` numbering counts pattern segments
12
+ only, as documented. Previously `a.(x,y).*` numbered `$0='a'` (a
13
+ literal), and `**.c` captured the literal `c`.
14
+
15
+ ### Changed
16
+ - A group is one pattern segment: it captures the path segments its
17
+ matching branch consumed as a single group, keeping `$N` positions
18
+ stable across branches of different lengths. `x.(a.b,c)` matching
19
+ `x.a.b` now captures `('x', 'a.b')` instead of `('x', 'a', 'b')`.
20
+ Parentheses thereby act as a regex-like capture group: `x.(a.b)`
21
+ captures `'a.b'` as one group where `x.a.b` captures `'a', 'b'`.
22
+ Nested patterns inside a branch fold into the group's capture, as
23
+ with `**`.
24
+
25
+ ## [0.44.6]
26
+
27
+ ### Added
28
+ - `match()` supports op groups: disjunction `(a,b)`, first-match `(a,b)?`,
29
+ conjunction `(a&b)`, and negation `(!a)` now match paths instead of
30
+ raising `AttributeError`. A path matches a group if it matches any
31
+ branch followed by the rest of the pattern (a concrete path is only
32
+ ever one branch's output, so Or/First/And all reduce to this);
33
+ negation matches one segment its inner pattern does not.
34
+ Multi-segment branches, nested groups, recursive ops inside branches,
35
+ cut markers, and `groups=True` captures all supported.
36
+
37
+ ### Fixed
38
+ - Wrapped variadic patterns in `match()`: `~(a,b)` (nop-wrapped group)
39
+ silently matched nothing and `**=7` (value-guarded recursive) raised
40
+ `AttributeError`; both now match.
41
+
42
+ ### Changed
43
+ - Path matching is polymorphic: `base.match_ops()` dispatches to each
44
+ op's `do_match`, which decides how many path segments it consumes
45
+ (single-segment default; backtracking on `Recursive`; branch
46
+ expansion on groups; `Wrap` delegates to variadic inners). The api
47
+ layer no longer special-cases op types.
48
+
6
49
  ## [0.44.5]
7
50
 
8
51
  ### Performance
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.44.5
3
+ Version: 0.44.7
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,17 @@ 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
+
511
522
  <a id="replace"></a>
512
523
  ### Replace
513
524
 
@@ -468,6 +468,17 @@ 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
+
471
482
  <a id="replace"></a>
472
483
  ### Replace
473
484
 
@@ -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,29 @@ 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
+
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
+
794
772
  Recursive patterns:
795
773
  >>> match('**.c', 'a.b.c')
796
774
  'a.b.c'
@@ -812,15 +790,13 @@ def match(pattern, path, groups=False, partial=True, strict=False):
812
790
  pats = parse(pattern)
813
791
  path_ops = parse(path)
814
792
 
815
- # Check if any pattern op is Recursive — use new recursive matcher
816
- has_recursive = any(op.is_recursive() for op in pats)
817
-
818
- if has_recursive:
819
- result = _match_ops(list(pats), list(path_ops), partial)
793
+ # Variadic ops (recursive, groups) consume variable-length path
794
+ # segments — use the recursive matcher
795
+ if any(op.is_variadic() for op in pats):
796
+ result = base.match_ops(list(pats), list(path_ops), partial)
820
797
  if result is None:
821
798
  return returns(None, [])
822
- # TODO: pattern-only filtering for recursive matches
823
- return returns(path, result)
799
+ return returns(path, [v for v, _ in result], [p for _, p in result])
824
800
 
825
801
  # Original non-recursive match logic
826
802
  _matches = []
@@ -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 (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.
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,25 @@ 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
+ is_pat = self.is_pattern()
225
+ if isinstance(m, (tuple, list)):
226
+ return [(_m.val, is_pat) for _m in m] + rest
227
+ return [(m.val, is_pat)] + rest
228
+
188
229
 
189
230
  class MatchOp(Op):
190
231
  """
@@ -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,32 @@ 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
+ 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
+
102
131
  def _render(self, top=True):
103
132
  """
104
133
  Render the group as a string. Subclasses override this.
@@ -518,6 +547,25 @@ class OpGroupNot(OpGroup):
518
547
  stack.push(base.Frame(frame.ops, v, cp, kwargs=frame.kwargs))
519
548
  return ()
520
549
 
550
+ def do_match(self, rest_pats, path_ops, partial):
551
+ """
552
+ Match one path segment NOT matched by the inner pattern's first op,
553
+ then continue with the rest of the inner branch and remaining ops.
554
+ """
555
+ inner = self.inner
556
+ if not inner:
557
+ return None
558
+ if not path_ops:
559
+ return None
560
+ kop = path_ops[0]
561
+ if base.match_ops([inner[0]], [kop], False) is not None:
562
+ return None
563
+ rest = base.match_ops(list(inner[1:]) + list(rest_pats), path_ops[1:], partial)
564
+ if rest is None:
565
+ return None
566
+ seg_val = getattr(getattr(kop, 'op', kop), 'value', kop)
567
+ return [(seg_val, True)] + rest
568
+
521
569
  def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, **kwargs):
522
570
  inner = self.inner
523
571
  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, True)] + 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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.44.5
3
+ Version: 0.44.7
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,17 @@ 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
+
511
522
  <a id="replace"></a>
512
523
  ### Replace
513
524
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dotted_notation"
7
- version = "0.44.5"
7
+ version = "0.44.7"
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"