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.
Files changed (38) hide show
  1. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/CHANGELOG.md +24 -0
  2. {dotted_notation-0.44.5/dotted_notation.egg-info → dotted_notation-0.44.6}/PKG-INFO +1 -1
  3. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/api.py +21 -51
  4. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/base.py +40 -0
  5. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/groups.py +36 -0
  6. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/recursive.py +22 -0
  7. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/wrappers.py +12 -0
  8. {dotted_notation-0.44.5 → dotted_notation-0.44.6/dotted_notation.egg-info}/PKG-INFO +1 -1
  9. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/pyproject.toml +1 -1
  10. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/LICENSE +0 -0
  11. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/MANIFEST.in +0 -0
  12. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/README.md +0 -0
  13. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/__init__.py +0 -0
  14. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/__main__.py +0 -0
  15. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/access.py +0 -0
  16. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/cli/__init__.py +0 -0
  17. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/cli/_compat.py +0 -0
  18. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/cli/formats.py +0 -0
  19. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/cli/main.py +0 -0
  20. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/containers.py +0 -0
  21. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/engine.py +0 -0
  22. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/filters.py +0 -0
  23. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/grammar.py +0 -0
  24. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/matchers.py +0 -0
  25. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/predicates.py +0 -0
  26. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/results.py +0 -0
  27. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/sql/__init__.py +0 -0
  28. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/sql/core.py +0 -0
  29. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/sql/pg.py +0 -0
  30. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/transforms.py +0 -0
  31. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/utils.py +0 -0
  32. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted/utypes.py +0 -0
  33. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted_notation.egg-info/SOURCES.txt +0 -0
  34. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted_notation.egg-info/dependency_links.txt +0 -0
  35. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted_notation.egg-info/entry_points.txt +0 -0
  36. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted_notation.egg-info/requires.txt +0 -0
  37. {dotted_notation-0.44.5 → dotted_notation-0.44.6}/dotted_notation.egg-info/top_level.txt +0 -0
  38. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.44.5
3
+ Version: 0.44.6
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
@@ -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
- # 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)
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 recursive matches
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.44.5
3
+ Version: 0.44.6
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
@@ -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.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"