dotted-notation 0.44.4__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.4 → dotted_notation-0.44.6}/CHANGELOG.md +32 -0
  2. {dotted_notation-0.44.4/dotted_notation.egg-info → dotted_notation-0.44.6}/PKG-INFO +1 -1
  3. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/api.py +28 -52
  4. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/base.py +40 -0
  5. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/groups.py +36 -0
  6. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/recursive.py +22 -0
  7. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/results.py +6 -5
  8. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/wrappers.py +12 -0
  9. {dotted_notation-0.44.4 → dotted_notation-0.44.6/dotted_notation.egg-info}/PKG-INFO +1 -1
  10. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/pyproject.toml +1 -1
  11. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/LICENSE +0 -0
  12. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/MANIFEST.in +0 -0
  13. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/README.md +0 -0
  14. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/__init__.py +0 -0
  15. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/__main__.py +0 -0
  16. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/access.py +0 -0
  17. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/cli/__init__.py +0 -0
  18. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/cli/_compat.py +0 -0
  19. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/cli/formats.py +0 -0
  20. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/cli/main.py +0 -0
  21. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/containers.py +0 -0
  22. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/engine.py +0 -0
  23. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/filters.py +0 -0
  24. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/grammar.py +0 -0
  25. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/matchers.py +0 -0
  26. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/predicates.py +0 -0
  27. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/sql/__init__.py +0 -0
  28. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/sql/core.py +0 -0
  29. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/sql/pg.py +0 -0
  30. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/transforms.py +0 -0
  31. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/utils.py +0 -0
  32. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted/utypes.py +0 -0
  33. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted_notation.egg-info/SOURCES.txt +0 -0
  34. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted_notation.egg-info/dependency_links.txt +0 -0
  35. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted_notation.egg-info/entry_points.txt +0 -0
  36. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted_notation.egg-info/requires.txt +0 -0
  37. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/dotted_notation.egg-info/top_level.txt +0 -0
  38. {dotted_notation-0.44.4 → dotted_notation-0.44.6}/setup.cfg +0 -0
@@ -3,6 +3,38 @@
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
+
30
+ ## [0.44.5]
31
+
32
+ ### Performance
33
+ - Transforms no longer disqualify the get() fast path: `a.b|int`
34
+ resolves via the direct-lookup chain and applies transforms on the
35
+ hit (~7x faster than walk). Plain paths are unaffected; `is_simple()`
36
+ semantics unchanged (still requires no transforms).
37
+
6
38
  ## [0.44.4]
7
39
 
8
40
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.44.4
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
@@ -287,6 +287,8 @@ def is_simple(path):
287
287
  False
288
288
  """
289
289
  parsed = path if isinstance(path, results.Dotted) else parse(path)
290
+ if parsed.transforms:
291
+ return False
290
292
  return parsed.simple_chain is not None
291
293
 
292
294
 
@@ -451,7 +453,11 @@ def get(obj, path, default=None, pattern_default=(), apply_transforms=True, stri
451
453
  if chain is not None:
452
454
  val = engine.simple_get(chain, obj, strict=strict)
453
455
  if val is not engine.SIMPLE_BAIL:
454
- return default if val is base.marker else val
456
+ if val is base.marker:
457
+ return default
458
+ if apply_transforms and ops.transforms:
459
+ return ops.apply(val)
460
+ return val
455
461
  vals = engine.iter_until_cut(engine.gets(ops, obj, strict=strict))
456
462
  if apply_transforms:
457
463
  vals = ( ops.apply(v) for v in vals )
@@ -716,51 +722,6 @@ def remove_multi(obj, iterable, paths_only=True, mutable=True, strict=False, bin
716
722
  return remove_if_multi(obj, iterable, paths_only=False, pred=None, mutable=mutable, strict=strict, bindings=bindings)
717
723
 
718
724
 
719
- def _match_ops(pats, path_ops, partial):
720
- """
721
- Recursive match of pattern ops against path ops.
722
- Returns list of match values on success, None on failure.
723
- Handles Recursive ops which can consume variable-length path segments.
724
- """
725
- if not pats:
726
- if not path_ops:
727
- return []
728
- if partial:
729
- return []
730
- return None
731
-
732
- pop = pats[0]
733
- rest_pats = pats[1:]
734
-
735
- # Non-recursive op: consume exactly one path segment
736
- if not pop.is_recursive():
737
- if not path_ops:
738
- return None
739
- kop = path_ops[0]
740
- m = pop.match(kop, specials=True)
741
- if not m:
742
- return None
743
- rest_result = _match_ops(rest_pats, path_ops[1:], partial)
744
- if rest_result is None:
745
- return None
746
- if isinstance(m, (tuple, list)):
747
- return [_m.val for _m in m] + rest_result
748
- return [m.val] + rest_result
749
-
750
- # Recursive op: try consuming 1, 2, ... N path segments via backtracking
751
- for n in range(1, len(path_ops) + 1):
752
- kop = path_ops[n - 1]
753
- seg_val = getattr(getattr(kop, 'op', kop), 'value', kop)
754
- matched = any(True for _ in pop.inner.matches((seg_val,)))
755
- if not matched:
756
- break # chain-following: stop extending once a segment fails
757
- rest_result = _match_ops(rest_pats, path_ops[n:], partial)
758
- if rest_result is not None:
759
- combined = results.assemble(path_ops[:n])
760
- return [combined] + rest_result
761
- return None
762
-
763
-
764
725
  def match(pattern, path, groups=False, partial=True, strict=False):
765
726
  """
766
727
  Returns `path` if `pattern` matches; otherwise `None`
@@ -785,6 +746,22 @@ def match(pattern, path, groups=False, partial=True, strict=False):
785
746
  >>> match('hello.*', 'hello.there.bye', groups='patterns')
786
747
  ('hello.there.bye', ('there.bye',))
787
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
+
788
765
  Recursive patterns:
789
766
  >>> match('**.c', 'a.b.c')
790
767
  'a.b.c'
@@ -806,14 +783,13 @@ def match(pattern, path, groups=False, partial=True, strict=False):
806
783
  pats = parse(pattern)
807
784
  path_ops = parse(path)
808
785
 
809
- # Check if any pattern op is Recursive — use new recursive matcher
810
- has_recursive = any(op.is_recursive() for op in pats)
811
-
812
- if has_recursive:
813
- 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)
814
790
  if result is None:
815
791
  return returns(None, [])
816
- # TODO: pattern-only filtering for recursive matches
792
+ # TODO: pattern-only filtering for variadic matches
817
793
  return returns(path, result)
818
794
 
819
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.
@@ -113,13 +113,14 @@ class Dotted:
113
113
  @lazyprop
114
114
  def simple_chain(self):
115
115
  """
116
- Tuple of (kind, key) pairs when this path is simple — a plain chain
117
- of concrete Key/Attr/Slot accesses with no patterns, substitutions,
118
- references, guards, transforms, or filters — else None. Computed
119
- once and cached; drives the fast path in get() that skips walk().
116
+ Tuple of (kind, key) pairs when the access chain is simple — a plain
117
+ chain of concrete Key/Attr/Slot accesses with no patterns,
118
+ substitutions, references, guards, or filters — else None. Transforms
119
+ are permitted (they apply after the lookup). Computed once and
120
+ cached; drives the fast path in get() that skips walk().
120
121
  kind is 'key', 'attr', or 'slot'.
121
122
  """
122
- if self.transforms or self.guard is not None:
123
+ if self.guard is not None:
123
124
  return None
124
125
  chain = []
125
126
  for op in self.ops:
@@ -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.4
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.4"
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"