dotted-notation 0.49.1__tar.gz → 0.49.3__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 (48) hide show
  1. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/CHANGELOG.md +54 -0
  2. {dotted_notation-0.49.1/dotted_notation.egg-info → dotted_notation-0.49.3}/PKG-INFO +44 -4
  3. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/README.md +42 -3
  4. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/access.py +71 -4
  5. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/api.py +5 -4
  6. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/base.py +60 -6
  7. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/containers.py +159 -11
  8. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/filters.py +35 -9
  9. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/grammar.py +10 -4
  10. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/recursive.py +32 -3
  11. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/results.py +17 -9
  12. {dotted_notation-0.49.1 → dotted_notation-0.49.3/dotted_notation.egg-info}/PKG-INFO +44 -4
  13. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/pyproject.toml +3 -2
  14. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/setup.py +4 -1
  15. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/LICENSE +0 -0
  16. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/MANIFEST.in +0 -0
  17. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/__init__.py +0 -0
  18. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/__main__.py +0 -0
  19. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/access.pxd +0 -0
  20. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/base.pxd +0 -0
  21. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/cli/__init__.py +0 -0
  22. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/cli/_compat.py +0 -0
  23. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/cli/formats.py +0 -0
  24. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/cli/main.py +0 -0
  25. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/engine.pxd +0 -0
  26. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/engine.py +0 -0
  27. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/filters.pxd +0 -0
  28. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/groups.pxd +0 -0
  29. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/groups.py +0 -0
  30. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/matchers.pxd +0 -0
  31. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/matchers.py +0 -0
  32. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/native.py +0 -0
  33. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/predicates.py +0 -0
  34. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/recursive.pxd +0 -0
  35. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/sql/__init__.py +0 -0
  36. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/sql/core.py +0 -0
  37. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/sql/pg.py +0 -0
  38. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/transforms.py +0 -0
  39. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/utils.py +0 -0
  40. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/utypes.py +0 -0
  41. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/wrappers.pxd +0 -0
  42. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted/wrappers.py +0 -0
  43. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted_notation.egg-info/SOURCES.txt +0 -0
  44. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted_notation.egg-info/dependency_links.txt +0 -0
  45. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted_notation.egg-info/entry_points.txt +0 -0
  46. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted_notation.egg-info/requires.txt +0 -0
  47. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/dotted_notation.egg-info/top_level.txt +0 -0
  48. {dotted_notation-0.49.1 → dotted_notation-0.49.3}/setup.cfg +0 -0
@@ -3,6 +3,60 @@
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.49.3]
7
+
8
+ ### Added
9
+ - A slice bound may be a reference or a substitution: `b[:$$(i)]`,
10
+ `b[$(n):]`, `b[$0:$1]`, `b[::$$(step)]`. It resolves as a slot's does,
11
+ so `b[:$$(i)]|sum` is a running total with `i` bound per element
12
+ (#64). A reference that does not resolve matches nothing.
13
+ - A reference may hold parentheses of its own: `$$()` runs to the `)`
14
+ that closes it, so a group or a pack works inside, `$$(a.(b,c))`,
15
+ `$$((a[*];))`. A `)` inside quotes does not count.
16
+ - A reference inside a container value, `=[1, $$(x)]`, `={"k": $$(x)}`,
17
+ `="ab"...$$(x)`, `=(1, $$(x))`, resolves as a whole guard or filter
18
+ value does. One whose path is a pattern branches the container, once
19
+ per value; pack it, `$$((x[*];))`, to compare with the tuple instead.
20
+ In a string or bytes glob it must resolve to `str` or `bytes`, else
21
+ `TypeError`; `$$(x|str)` coerces.
22
+ - A reference in a filter key part, `[*&$$(field)="bob"]`,
23
+ `[*&tags[$$(^^i)]="b"]`, `[*&xs[:$$(^^n)]=[1]]`, resolves against the
24
+ node that part is looked up in, as `^`; in the first part that is the
25
+ item. A pattern reference tries each key it resolves to.
26
+
27
+ ### Fixed
28
+ - An unresolved substitution inside a container value, `=[1, $(x)]`,
29
+ makes the path an unresolved template, so `get` and the rest raise
30
+ `TypeError` as for any other unresolved `$N`, rather than matching
31
+ nothing.
32
+ - Resolving a recursive op's substitutions, `**&x=$(v)` or
33
+ `*($(k), [*])&x=1`, now resolves those in its filters, keeps the
34
+ filters, and keeps `*?`/`**?` first-match. `is_template` and
35
+ `is_reference` see a recursive op's accessors and filters, so an
36
+ unresolved `$N` there raises and a `$$(^^…)` gets its parents.
37
+ - Resolving `$N` in a string or bytes glob no longer turns a `$$(path)`
38
+ part into the literal text of its path.
39
+ - A slice's repr shows its step, `[1:5:2]`, rather than repeating its
40
+ stop.
41
+ - `make native` on macOS, and on Linux from Python 3.14: `setup.py`
42
+ runs `setup()` under a `__main__` guard, since Cython's spawned
43
+ workers import it again.
44
+
45
+ ## [0.49.2]
46
+
47
+ ### Added
48
+ - `ReferenceResolver.literals` may name the positions a resolver is
49
+ offered plain tokens in, as a list or tuple of `'segments'`,
50
+ `'transforms'`, `'guards'` and `'filters'`. A host whose variables are
51
+ sigiled arguments, `|add:^fee`, leaves `'segments'` out and no longer
52
+ pays a call per key (#63); `get` then keeps its fast path for a plain
53
+ chain of keys. An unknown name is a `ValueError`. `True` and `False`
54
+ are unchanged.
55
+ - `ReferenceResolver(literals=...)` sets `literals` per instance,
56
+ overriding the class's, so a resolver need not be subclassed only to
57
+ change it. A subclass with its own `__init__` passes it on with
58
+ `super().__init__(literals)`.
59
+
6
60
  ## [0.49.1]
7
61
 
8
62
  ### Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.49.1
3
+ Version: 0.49.3
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-Expression: MIT
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: 3.15
24
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
26
  Classifier: Topic :: Utilities
26
27
  Requires-Python: >=3.6
@@ -1163,6 +1164,14 @@ Dotted slicing works like python slicing and all that entails.
1163
1164
  >>> dotted.get(d, '*.there[1:]')
1164
1165
  ([2, 3], [5, 6])
1165
1166
 
1167
+ A bound may be a reference or a substitution, resolved the way a slot's is:
1168
+
1169
+ >>> d = {'b': [3, 0, 5], 'i': 2}
1170
+ >>> dotted.get(d, 'b[:$$(i)]')
1171
+ [3, 0]
1172
+ >>> dotted.get(d, 'b[$(n):]', bindings={'n': 1})
1173
+ [0, 5]
1174
+
1166
1175
  <a id="dot-notation-for-sequence-indexing"></a>
1167
1176
  ### Dot notation for sequence indexing
1168
1177
 
@@ -1641,6 +1650,23 @@ as do the transforms before a guard:
1641
1650
  >>> dotted.get(data, '*|mul:$$(scale)=20')
1642
1651
  (2,)
1643
1652
 
1653
+ So does a reference inside a container value. One whose path is a pattern
1654
+ branches the container, once per value; pack it, `(…;)`, to compare with the
1655
+ tuple instead:
1656
+
1657
+ >>> data = {'want': [2, 3], 'items': [[1, 2], [1, 4], [1, (2, 3)]]}
1658
+ >>> dotted.get(data, 'items[*]=[1, $$(want[*])]')
1659
+ ([1, 2],)
1660
+ >>> dotted.get(data, 'items[*]=[1, $$((want[*];))]')
1661
+ ([1, (2, 3)],)
1662
+
1663
+ A filter key may hold references too. In each part of the key, `^` is the
1664
+ node that part is looked up in, so in the first part it is the item:
1665
+
1666
+ >>> data = {'field': 'name', 'users': [{'name': 'alice'}, {'name': 'bob'}]}
1667
+ >>> dotted.get(data, 'users[*&$$(field)="bob"]')
1668
+ ({'name': 'bob'},)
1669
+
1644
1670
  With `update`, the transforms apply to the value being set, so `^` is that
1645
1671
  value:
1646
1672
 
@@ -1677,14 +1703,16 @@ claim some references and leave the rest:
1677
1703
  >>> dotted.get({'amount': 10}, 'amount|sub:$$(lots.taken)', resolver=Store(store))
1678
1704
  7
1679
1705
 
1680
- A resolver that sets `literals = True` is also offered every plain token in
1681
- those positions, so a host sigil such as `$sel` can be a variable. Return
1706
+ A resolver with `literals = True` is also offered every plain token in
1707
+ those positions, so a host sigil such as `$sel` can be a variable. Set it on
1708
+ the class, or per instance with `literals=` on the constructor. Return
1682
1709
  `ref.value` to leave a token the literal it is; a key the resolver answers
1683
1710
  is not offered again:
1684
1711
 
1685
1712
  >>> class Sigils(dotted.ReferenceResolver):
1686
1713
  ... literals = True
1687
- ... def __init__(self, names):
1714
+ ... def __init__(self, names, literals=None):
1715
+ ... super().__init__(literals)
1688
1716
  ... self.names = names
1689
1717
  ... def resolve(self, ref, settings, node=None, parents=()):
1690
1718
  ... if ref.is_reference():
@@ -1696,6 +1724,18 @@ is not offered again:
1696
1724
  >>> dotted.get(users, 'users[*&name=$sel].name', resolver=Sigils({'$sel': 'bob'}))
1697
1725
  ('bob',)
1698
1726
 
1727
+ `literals` may instead name the positions it wants, as a list or tuple of
1728
+ `'segments'` (key, slot and attr segments), `'transforms'` (transform
1729
+ arguments), `'guards'` (guard values) and `'filters'` (filter values). A host
1730
+ whose variables only ever stand as arguments leaves `'segments'` out, so the
1731
+ keys of a path cost no call:
1732
+
1733
+ >>> r = Sigils({'$inc': 3}, literals=('transforms', 'guards', 'filters'))
1734
+ >>> dotted.get({'n': 5}, 'n|add:$inc', resolver=r)
1735
+ 8
1736
+
1737
+ An unknown name is a `ValueError`, on the class or the constructor.
1738
+
1699
1739
  A host variable may span several segments, `$amount.value`, which dotted has
1700
1740
  parsed as two. The path-level hook `fold(segments)` is called once with a
1701
1741
  path's segment strings, `('$amount', 'value')`, and returns the runs it
@@ -1116,6 +1116,14 @@ Dotted slicing works like python slicing and all that entails.
1116
1116
  >>> dotted.get(d, '*.there[1:]')
1117
1117
  ([2, 3], [5, 6])
1118
1118
 
1119
+ A bound may be a reference or a substitution, resolved the way a slot's is:
1120
+
1121
+ >>> d = {'b': [3, 0, 5], 'i': 2}
1122
+ >>> dotted.get(d, 'b[:$$(i)]')
1123
+ [3, 0]
1124
+ >>> dotted.get(d, 'b[$(n):]', bindings={'n': 1})
1125
+ [0, 5]
1126
+
1119
1127
  <a id="dot-notation-for-sequence-indexing"></a>
1120
1128
  ### Dot notation for sequence indexing
1121
1129
 
@@ -1594,6 +1602,23 @@ as do the transforms before a guard:
1594
1602
  >>> dotted.get(data, '*|mul:$$(scale)=20')
1595
1603
  (2,)
1596
1604
 
1605
+ So does a reference inside a container value. One whose path is a pattern
1606
+ branches the container, once per value; pack it, `(…;)`, to compare with the
1607
+ tuple instead:
1608
+
1609
+ >>> data = {'want': [2, 3], 'items': [[1, 2], [1, 4], [1, (2, 3)]]}
1610
+ >>> dotted.get(data, 'items[*]=[1, $$(want[*])]')
1611
+ ([1, 2],)
1612
+ >>> dotted.get(data, 'items[*]=[1, $$((want[*];))]')
1613
+ ([1, (2, 3)],)
1614
+
1615
+ A filter key may hold references too. In each part of the key, `^` is the
1616
+ node that part is looked up in, so in the first part it is the item:
1617
+
1618
+ >>> data = {'field': 'name', 'users': [{'name': 'alice'}, {'name': 'bob'}]}
1619
+ >>> dotted.get(data, 'users[*&$$(field)="bob"]')
1620
+ ({'name': 'bob'},)
1621
+
1597
1622
  With `update`, the transforms apply to the value being set, so `^` is that
1598
1623
  value:
1599
1624
 
@@ -1630,14 +1655,16 @@ claim some references and leave the rest:
1630
1655
  >>> dotted.get({'amount': 10}, 'amount|sub:$$(lots.taken)', resolver=Store(store))
1631
1656
  7
1632
1657
 
1633
- A resolver that sets `literals = True` is also offered every plain token in
1634
- those positions, so a host sigil such as `$sel` can be a variable. Return
1658
+ A resolver with `literals = True` is also offered every plain token in
1659
+ those positions, so a host sigil such as `$sel` can be a variable. Set it on
1660
+ the class, or per instance with `literals=` on the constructor. Return
1635
1661
  `ref.value` to leave a token the literal it is; a key the resolver answers
1636
1662
  is not offered again:
1637
1663
 
1638
1664
  >>> class Sigils(dotted.ReferenceResolver):
1639
1665
  ... literals = True
1640
- ... def __init__(self, names):
1666
+ ... def __init__(self, names, literals=None):
1667
+ ... super().__init__(literals)
1641
1668
  ... self.names = names
1642
1669
  ... def resolve(self, ref, settings, node=None, parents=()):
1643
1670
  ... if ref.is_reference():
@@ -1649,6 +1676,18 @@ is not offered again:
1649
1676
  >>> dotted.get(users, 'users[*&name=$sel].name', resolver=Sigils({'$sel': 'bob'}))
1650
1677
  ('bob',)
1651
1678
 
1679
+ `literals` may instead name the positions it wants, as a list or tuple of
1680
+ `'segments'` (key, slot and attr segments), `'transforms'` (transform
1681
+ arguments), `'guards'` (guard values) and `'filters'` (filter values). A host
1682
+ whose variables only ever stand as arguments leaves `'segments'` out, so the
1683
+ keys of a path cost no call:
1684
+
1685
+ >>> r = Sigils({'$inc': 3}, literals=('transforms', 'guards', 'filters'))
1686
+ >>> dotted.get({'n': 5}, 'n|add:$inc', resolver=r)
1687
+ 8
1688
+
1689
+ An unknown name is a `ValueError`, on the class or the constructor.
1690
+
1652
1691
  A host variable may span several segments, `$amount.value`, which dotted has
1653
1692
  parsed as two. The path-level hook `fold(segments)` is called once with a
1654
1693
  path's segment strings, `('$amount', 'value')`, and returns the runs it
@@ -251,11 +251,12 @@ class AccessOp(SimpleOp):
251
251
  def _offered(self, settings):
252
252
  """
253
253
  True if the match op goes to the resolver: a reference, or a
254
- plain constant when the resolver in force takes literals.
254
+ plain constant when the resolver in force takes literals in path
255
+ segments.
255
256
  """
256
257
  if self.op.is_reference():
257
258
  return True
258
- return (settings.literals() and isinstance(self.op, matchers.Const)
259
+ return (settings.literals('segments') and isinstance(self.op, matchers.Const)
259
260
  and not isinstance(self.op, matchers.ResolvedValue))
260
261
 
261
262
  def resolve(self, bindings, partial=False):
@@ -993,12 +994,49 @@ class Slice(SimpleOp):
993
994
  start, stop, step = self.args
994
995
  m = [s(start), s(stop)]
995
996
  if step is not None:
996
- m += [s(stop)]
997
+ m += [s(step)]
997
998
  return '[' + ':'.join(m) + ']'
998
999
  def is_pattern(self):
999
1000
  return False
1000
1001
  def is_slice(self):
1001
1002
  return True
1003
+ def is_template(self):
1004
+ """
1005
+ True if a bound is an unresolved substitution.
1006
+ """
1007
+ return any(isinstance(a, matchers.Subst) for a in self.args)
1008
+ def is_reference(self):
1009
+ """
1010
+ True if a bound is an internal reference.
1011
+ """
1012
+ return any(isinstance(a, matchers.Reference) for a in self.args)
1013
+ def resolve(self, bindings, partial=False):
1014
+ """
1015
+ Resolve $N in the bounds.
1016
+ """
1017
+ if not self.is_template():
1018
+ return self
1019
+ args = []
1020
+ for a in self.args:
1021
+ r = a.resolve(bindings, partial) if isinstance(a, matchers.Subst) else a
1022
+ # a resolved substitution is a ResolvedValue; the bound is its value
1023
+ args.append(r.value if isinstance(r, matchers.ResolvedValue) else r)
1024
+ return self.concrete(slice(*args))
1025
+ def _resolved(self, node=None, settings=base.SETTINGS):
1026
+ """
1027
+ This slice with its $$(path) bounds resolved, or None if one does
1028
+ not resolve.
1029
+ """
1030
+ args = []
1031
+ for a in self.args:
1032
+ if not isinstance(a, matchers.Reference):
1033
+ args.append(a)
1034
+ continue
1035
+ try:
1036
+ args.append(settings.resolve(a, node, settings.parents or ()))
1037
+ except KeyError:
1038
+ return None
1039
+ return self.concrete(slice(*args))
1002
1040
  def operator(self, top=False):
1003
1041
  return str(self)
1004
1042
  def slice(self, node=None):
@@ -1011,6 +1049,8 @@ class Slice(SimpleOp):
1011
1049
  Calculate cardinality of a slice; don't both dealing with countably infinite
1012
1050
  set arithmetic, instead just pick a suitably large integer
1013
1051
  """
1052
+ if self.is_template() or self.is_reference():
1053
+ return 0
1014
1054
  s = self.slice(node)
1015
1055
  start = s.start or 0
1016
1056
  stop = s.stop or '+'
@@ -1022,7 +1062,12 @@ class Slice(SimpleOp):
1022
1062
  return max(0, int((stop - start) / step))
1023
1063
 
1024
1064
  def keys(self, node, settings=base.SETTINGS):
1025
- return (self.slice(node),)
1065
+ if not self.is_reference():
1066
+ return (self.slice(node),)
1067
+ r = self._resolved(node=node, settings=settings)
1068
+ if r is None:
1069
+ return ()
1070
+ return r.keys(node, settings=settings)
1026
1071
  def items(self, node, settings=base.SETTINGS, filtered=True):
1027
1072
  return self._slice_items(node, settings)
1028
1073
  def _slice_items(self, node, settings):
@@ -1075,6 +1120,28 @@ class Slice(SimpleOp):
1075
1120
  def upsert(self, node, val):
1076
1121
  return self.update(node, self.slice(node), val)
1077
1122
 
1123
+ def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, settings=base.SETTINGS):
1124
+ """
1125
+ Resolve references before updating.
1126
+ """
1127
+ if not self.is_reference():
1128
+ return super().do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap, settings=settings)
1129
+ r = self._resolved(node=node, settings=settings)
1130
+ if r is None:
1131
+ return node
1132
+ return r.do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap, settings=settings)
1133
+
1134
+ def do_remove(self, ops, node, val, nop, settings=base.SETTINGS):
1135
+ """
1136
+ Resolve references before removing.
1137
+ """
1138
+ if not self.is_reference():
1139
+ return super().do_remove(ops, node, val, nop, settings=settings)
1140
+ r = self._resolved(node=node, settings=settings)
1141
+ if r is None:
1142
+ return node
1143
+ return r.do_remove(ops, node, val, nop, settings=settings)
1144
+
1078
1145
  def pop(self, node, key):
1079
1146
  try:
1080
1147
  del node[key]
@@ -448,9 +448,11 @@ def build(obj, path, strict=False, bindings=None, resolver=None):
448
448
  def _literals(resolver):
449
449
  """
450
450
  True if the resolver a call will run under, its own or the process
451
- default, takes plain tokens: the simple fast path must then step aside.
451
+ default, takes plain tokens in path segments: the simple fast path must
452
+ then step aside. Other positions it can serve, as its transforms
453
+ resolve through Settings.
452
454
  """
453
- return (base.RESOLVER if resolver is None else resolver).literals
455
+ return base.takes_literals(base.RESOLVER if resolver is None else resolver, 'segments')
454
456
 
455
457
 
456
458
  def _prepare(ops, resolver):
@@ -1365,12 +1367,11 @@ def set_resolver(resolver=None):
1365
1367
  restores dotted's own (see Resolver).
1366
1368
  >>> import dotted
1367
1369
  >>> class Upper(dotted.ReferenceResolver):
1368
- ... literals = True
1369
1370
  ... def resolve(self, ref, settings, node=None, parents=()):
1370
1371
  ... if ref.is_reference():
1371
1372
  ... return super().resolve(ref, settings, node, parents)
1372
1373
  ... return ref.value.upper()
1373
- >>> dotted.set_resolver(Upper())
1374
+ >>> dotted.set_resolver(Upper(literals=True))
1374
1375
  >>> dotted.get({'A': 1}, 'a')
1375
1376
  1
1376
1377
  >>> dotted.set_resolver()
@@ -149,6 +149,12 @@ class NOP(metaclass=MetaNOP):
149
149
  return False
150
150
 
151
151
 
152
+ # The positions a resolver may be offered plain tokens in, named in its
153
+ # literals: key, slot and attr segments, transform arguments, guard values
154
+ # and filter values.
155
+ LITERALS = ('segments', 'transforms', 'guards', 'filters')
156
+
157
+
152
158
  class Resolver:
153
159
  """
154
160
  What a reference means at run time. Every $$(path) a traversal meets,
@@ -161,10 +167,30 @@ class Resolver:
161
167
 
162
168
  # True to be offered every plain token in a var-permitting position as
163
169
  # well: key, slot and attr segments, transform arguments, guard and
164
- # filter values. The host decides which are its variables; returning
165
- # ref.value leaves a token the literal it is.
170
+ # filter values. A list or tuple of LITERALS names to be offered them in
171
+ # those positions only, ('transforms', 'guards', 'filters') to leave
172
+ # segments out. The host decides which are its variables; returning
173
+ # ref.value leaves a token the literal it is. Set here, on a subclass,
174
+ # or per instance with literals=.
166
175
  literals = False
167
176
 
177
+ def __init_subclass__(cls, **kwargs):
178
+ """
179
+ Check a subclass's literals (see check_literals).
180
+ """
181
+ super().__init_subclass__(**kwargs)
182
+ check_literals(cls.literals)
183
+
184
+ def __init__(self, literals=None):
185
+ """
186
+ literals, when given, overrides the class's for this instance (see
187
+ check_literals).
188
+ """
189
+ if literals is None:
190
+ return
191
+ check_literals(literals)
192
+ self.literals = literals
193
+
168
194
  def fold(self, segments):
169
195
  """
170
196
  The path-level hook, called once with a path's segment strings
@@ -211,6 +237,33 @@ class Resolver:
211
237
  RESOLVER = Resolver()
212
238
 
213
239
 
240
+ def check_literals(literals):
241
+ """
242
+ Raise unless literals is True, False, or a list or tuple of LITERALS
243
+ names.
244
+ """
245
+ if isinstance(literals, bool):
246
+ return
247
+ if not isinstance(literals, (list, tuple)):
248
+ raise TypeError(f'literals is True, False or a list or tuple of {LITERALS}, not {literals!r}')
249
+ unknown = [name for name in literals if name not in LITERALS]
250
+ if unknown:
251
+ raise ValueError(f'literals names {unknown}, not among {LITERALS}')
252
+
253
+
254
+ def takes_literals(resolver, position):
255
+ """
256
+ True if resolver takes plain tokens at position, one of LITERALS: its
257
+ literals is True, or names it.
258
+ """
259
+ lit = resolver.literals
260
+ if lit is True:
261
+ return True
262
+ if not lit:
263
+ return False
264
+ return position in lit
265
+
266
+
214
267
  class Settings:
215
268
  """
216
269
  What holds for the whole of one traversal: the root node, the chain of
@@ -242,11 +295,12 @@ class Settings:
242
295
  """
243
296
  return RESOLVER if self.resolver is None else self.resolver
244
297
 
245
- def literals(self):
298
+ def literals(self, position):
246
299
  """
247
- True if the resolver in force wants plain tokens offered too.
300
+ True if the resolver in force wants plain tokens at position, one
301
+ of LITERALS, offered too.
248
302
  """
249
- return self.resolving().literals
303
+ return takes_literals(self.resolving(), position)
250
304
 
251
305
  def resolve(self, ref, node=None, parents=()):
252
306
  """
@@ -551,7 +605,7 @@ class Transform(Op):
551
605
  return ()
552
606
  if settings is None:
553
607
  settings = SETTINGS
554
- literals = settings.literals()
608
+ literals = settings.literals('transforms')
555
609
  args = []
556
610
  for p in self.params:
557
611
  if isinstance(p, Op) and (p.is_reference() or (literals and isinstance(p, matchers.Const))):
@@ -25,7 +25,7 @@ Type prefix semantics:
25
25
  fs{...} frozenset only
26
26
  """
27
27
  import re
28
- from itertools import combinations
28
+ from itertools import combinations, product
29
29
 
30
30
  from . import utils
31
31
  from . import base
@@ -46,11 +46,91 @@ def _element_matches(pattern, val):
46
46
  return any(True for _ in pattern.matches((val,)))
47
47
 
48
48
 
49
+ def _holds(items, method):
50
+ """
51
+ True if an item, or a part of a (key, value) dict entry, answers True
52
+ to method: 'is_template' or 'is_reference'.
53
+ """
54
+ for x in items:
55
+ if isinstance(x, tuple):
56
+ if _holds(x, method):
57
+ return True
58
+ continue
59
+ test = getattr(x, method, None)
60
+ if test is not None and test():
61
+ return True
62
+ return False
63
+
64
+
65
+ def _values(ref, settings, node, parents):
66
+ """
67
+ What reference ref resolves to, as a tuple: its value, or each value
68
+ of a pattern reference.
69
+ """
70
+ val = settings.resolve(ref, node, parents)
71
+ if ref.is_pattern():
72
+ return tuple(val)
73
+ return (val,)
74
+
75
+
76
+ def _alternatives(e, settings, node, parents):
77
+ """
78
+ The forms element e takes once its references resolve: a Const per
79
+ value of a reference, a nested container's branches, each pairing of
80
+ a dict entry's key and value forms; just e when it holds none.
81
+ """
82
+ if isinstance(e, tuple):
83
+ return tuple(product(*(_alternatives(p, settings, node, parents) for p in e)))
84
+ if not _holds((e,), 'is_reference'):
85
+ return (e,)
86
+ if isinstance(e, matchers.Reference):
87
+ return tuple(matchers.Const(v) for v in _values(e, settings, node, parents))
88
+ return e.branches(settings, node, parents)
89
+
90
+
91
+ def _fragments(part, typ, owner, settings, node, parents):
92
+ """
93
+ The fragments a string or bytes glob part stands for: a reference's
94
+ values, each of which must be a typ; any other part as it is.
95
+ """
96
+ if not isinstance(part, matchers.Reference):
97
+ return (part,)
98
+ vals = _values(part, settings, node, parents)
99
+ for v in vals:
100
+ if not isinstance(v, typ):
101
+ raise TypeError(
102
+ f'{owner} requires {typ.__name__}, got {type(v).__name__}'
103
+ f' — use $$(path|{typ.__name__}) to coerce')
104
+ return vals
105
+
106
+
107
+ class ContainerOp(base.Op):
108
+ """
109
+ Base of the container value patterns. Their args are their parts, so
110
+ whether one holds a substitution or a reference is read from those.
111
+
112
+ A reference inside resolves as the whole value's would (see
113
+ results.guard_matcher). One that is a pattern branches the container:
114
+ branches() gives the container once per combination of values.
115
+ """
116
+ def is_template(self):
117
+ """
118
+ True if a part holds an unresolved substitution.
119
+ """
120
+ return _holds(self.args, 'is_template')
121
+
122
+ def is_reference(self):
123
+ """
124
+ True if a part holds an internal reference.
125
+ """
126
+ return _holds(self.args, 'is_reference')
127
+
128
+
49
129
  # ---------------------------------------------------------------------------
50
130
  # Glob — the ... element
51
131
  # ---------------------------------------------------------------------------
52
132
 
53
- class Glob(base.Op):
133
+ class Glob(ContainerOp):
54
134
  """
55
135
  The ... element: zero or more values with optional pattern and count.
56
136
 
@@ -87,6 +167,14 @@ class Glob(base.Op):
87
167
  return self
88
168
  return Glob(new_pat, self.min_count, self.max_count)
89
169
 
170
+ def branches(self, settings, node=None, parents=()):
171
+ """
172
+ This glob once per form of its pattern (see ContainerOp).
173
+ """
174
+ return tuple(
175
+ Glob(p, self.min_count, self.max_count)
176
+ for p in _alternatives(self.pattern, settings, node, parents))
177
+
90
178
  def __repr__(self):
91
179
  parts = ['...']
92
180
  if self.pattern is not None:
@@ -105,7 +193,7 @@ class Glob(base.Op):
105
193
  # DictGlobEntry — a glob entry in a dict pattern
106
194
  # ---------------------------------------------------------------------------
107
195
 
108
- class DictGlobEntry(base.Op):
196
+ class DictGlobEntry(ContainerOp):
109
197
  """
110
198
  A glob entry in a dict: glob on key side with value pattern.
111
199
 
@@ -141,6 +229,15 @@ class DictGlobEntry(base.Op):
141
229
  return self
142
230
  return DictGlobEntry(new_kg, new_vp)
143
231
 
232
+ def branches(self, settings, node=None, parents=()):
233
+ """
234
+ This entry once per pairing of its key glob's and value pattern's
235
+ forms (see ContainerOp).
236
+ """
237
+ keys = _alternatives(self.key_glob, settings, node, parents)
238
+ vals = _alternatives(self.val_pattern, settings, node, parents)
239
+ return tuple(DictGlobEntry(k, v) for k, v in product(keys, vals))
240
+
144
241
  def __repr__(self):
145
242
  val_repr = repr(self.val_pattern) if self.val_pattern is not None else ''
146
243
  key_repr = repr(self.key_glob)
@@ -156,7 +253,7 @@ class DictGlobEntry(base.Op):
156
253
  _LIST_TYPES = (list, tuple)
157
254
 
158
255
 
159
- class ContainerList(base.Op):
256
+ class ContainerList(ContainerOp):
160
257
  """
161
258
  List/tuple pattern: [elem, elem, ...].
162
259
 
@@ -213,6 +310,14 @@ class ContainerList(base.Op):
213
310
  return self
214
311
  return ContainerList(*new_elems, type_prefix=self.type_prefix)
215
312
 
313
+ def branches(self, settings, node=None, parents=()):
314
+ """
315
+ This list once per combination of its elements' forms (see
316
+ ContainerOp).
317
+ """
318
+ forms = (_alternatives(e, settings, node, parents) for e in self.elements)
319
+ return tuple(ContainerList(*c, type_prefix=self.type_prefix) for c in product(*forms))
320
+
216
321
  def __repr__(self):
217
322
  prefix = self.type_prefix or ''
218
323
  return prefix + '[' + ', '.join(repr(e) for e in self.elements) + ']'
@@ -222,7 +327,7 @@ class ContainerList(base.Op):
222
327
  # ContainerDict — {k: v, ...} pattern
223
328
  # ---------------------------------------------------------------------------
224
329
 
225
- class ContainerDict(base.Op):
330
+ class ContainerDict(ContainerOp):
226
331
  """
227
332
  Dict pattern: {key: val, key: val, ...}.
228
333
 
@@ -280,6 +385,14 @@ class ContainerDict(base.Op):
280
385
  return self
281
386
  return ContainerDict(*new_entries, type_prefix=self.type_prefix)
282
387
 
388
+ def branches(self, settings, node=None, parents=()):
389
+ """
390
+ This dict once per combination of its entries' forms (see
391
+ ContainerOp).
392
+ """
393
+ forms = (_alternatives(e, settings, node, parents) for e in self.entries)
394
+ return tuple(ContainerDict(*c, type_prefix=self.type_prefix) for c in product(*forms))
395
+
283
396
  def matchable(self, op, specials=False):
284
397
  """
285
398
  ContainerDict can match against Const values.
@@ -302,7 +415,7 @@ class ContainerDict(base.Op):
302
415
  # ContainerSet — {v, v, ...} pattern
303
416
  # ---------------------------------------------------------------------------
304
417
 
305
- class ContainerSet(base.Op):
418
+ class ContainerSet(ContainerOp):
306
419
  """
307
420
  Set/frozenset pattern: {elem, elem, ...}.
308
421
 
@@ -353,6 +466,14 @@ class ContainerSet(base.Op):
353
466
  return self
354
467
  return ContainerSet(*new_elems, type_prefix=self.type_prefix)
355
468
 
469
+ def branches(self, settings, node=None, parents=()):
470
+ """
471
+ This set once per combination of its elements' forms (see
472
+ ContainerOp).
473
+ """
474
+ forms = (_alternatives(e, settings, node, parents) for e in self.elements)
475
+ return tuple(ContainerSet(*c, type_prefix=self.type_prefix) for c in product(*forms))
476
+
356
477
  def matchable(self, op, specials=False):
357
478
  """
358
479
  ContainerSet can match against Const values.
@@ -368,7 +489,7 @@ class ContainerSet(base.Op):
368
489
  # StringGlob — "prefix"..."suffix" string pattern
369
490
  # ---------------------------------------------------------------------------
370
491
 
371
- class StringGlob(base.Op):
492
+ class StringGlob(ContainerOp):
372
493
  """
373
494
  String glob pattern: quoted fragments with ... between them.
374
495
 
@@ -437,7 +558,8 @@ class StringGlob(base.Op):
437
558
  new_parts = []
438
559
  changed = False
439
560
  for p in self.parts:
440
- if not hasattr(p, 'resolve'):
561
+ # a glob holds no substitution; a reference resolves when matched
562
+ if isinstance(p, (Glob, matchers.Reference)) or not hasattr(p, 'resolve'):
441
563
  new_parts.append(p)
442
564
  continue
443
565
  np = p.resolve(bindings, partial)
@@ -462,6 +584,14 @@ class StringGlob(base.Op):
462
584
  return self
463
585
  return StringGlob(*new_parts)
464
586
 
587
+ def branches(self, settings, node=None, parents=()):
588
+ """
589
+ This glob once per combination of the strings its references
590
+ resolve to (see ContainerOp).
591
+ """
592
+ forms = (_fragments(p, str, 'StringGlob', settings, node, parents) for p in self.parts)
593
+ return tuple(StringGlob(*c) for c in product(*forms))
594
+
465
595
  def matches(self, vals):
466
596
  """
467
597
  Yield str vals matching the glob pattern.
@@ -493,7 +623,7 @@ class StringGlob(base.Op):
493
623
  # BytesGlob — b"prefix"...b"suffix" bytes pattern
494
624
  # ---------------------------------------------------------------------------
495
625
 
496
- class BytesGlob(base.Op):
626
+ class BytesGlob(ContainerOp):
497
627
  """
498
628
  Bytes glob pattern: byte-string fragments with ... between them.
499
629
 
@@ -562,7 +692,8 @@ class BytesGlob(base.Op):
562
692
  new_parts = []
563
693
  changed = False
564
694
  for p in self.parts:
565
- if not hasattr(p, 'resolve'):
695
+ # a glob holds no substitution; a reference resolves when matched
696
+ if isinstance(p, (Glob, matchers.Reference)) or not hasattr(p, 'resolve'):
566
697
  new_parts.append(p)
567
698
  continue
568
699
  np = p.resolve(bindings, partial)
@@ -587,6 +718,14 @@ class BytesGlob(base.Op):
587
718
  return self
588
719
  return BytesGlob(*new_parts)
589
720
 
721
+ def branches(self, settings, node=None, parents=()):
722
+ """
723
+ This glob once per combination of the bytes its references
724
+ resolve to (see ContainerOp).
725
+ """
726
+ forms = (_fragments(p, bytes, 'BytesGlob', settings, node, parents) for p in self.parts)
727
+ return tuple(BytesGlob(*c) for c in product(*forms))
728
+
590
729
  def matches(self, vals):
591
730
  """
592
731
  Yield bytes vals matching the glob pattern.
@@ -618,7 +757,7 @@ class BytesGlob(base.Op):
618
757
  # ValueGroup — (val1, val2) value disjunction
619
758
  # ---------------------------------------------------------------------------
620
759
 
621
- class ValueGroup(base.Op):
760
+ class ValueGroup(ContainerOp):
622
761
  """
623
762
  Value group: (val1, val2, ...) — matches any alternative.
624
763
 
@@ -652,6 +791,15 @@ class ValueGroup(base.Op):
652
791
  return self
653
792
  return ValueGroup(*new_alts)
654
793
 
794
+ def branches(self, settings, node=None, parents=()):
795
+ """
796
+ This group with its references resolved: one group, whose
797
+ alternatives are every form its alternatives take (see
798
+ ContainerOp).
799
+ """
800
+ alts = (f for a in self.alternatives for f in _alternatives(a, settings, node, parents))
801
+ return (ValueGroup(*alts),)
802
+
655
803
  def matchable(self, op, specials=False):
656
804
  """
657
805
  ValueGroup can match against Const values.
@@ -2,6 +2,7 @@
2
2
  Filter ops: key-value filters, boolean combinators, and negation.
3
3
  """
4
4
  from . import base
5
+ from . import matchers
5
6
  from . import predicates
6
7
  from .access import Slot, Slice
7
8
 
@@ -112,29 +113,37 @@ class FilterKey(base.MatchOp):
112
113
  if hasattr(part, 'gather'):
113
114
  yield from self._get_values(part.gather(node, settings), rest, settings)
114
115
  return
116
+ # a reference in a part resolves against the node the part is looked
117
+ # up in, as ^, and that node's ancestors above it
118
+ below = settings.below(node)
115
119
  if isinstance(part, Slot):
120
+ # only a reference goes through settings: a resolver taking
121
+ # literals in segments is not offered a filter key's
122
+ slots = part._resolved(node=node, settings=settings) if part.is_reference() else (part,)
116
123
  try:
117
- for v in part.values(node):
118
- yield from self._get_values(v, rest, settings)
124
+ for slot in slots:
125
+ for v in slot.values(node):
126
+ yield from self._get_values(v, rest, below)
119
127
  except (TypeError, AttributeError):
120
128
  yield None, False
121
129
  return
122
130
  if isinstance(part, Slice):
123
131
  try:
124
- s = part.slice(node)
132
+ s = next(iter(part.keys(node, settings=settings)))
125
133
  val = node[s]
126
- yield from self._get_values(val, rest, settings)
127
- except (TypeError, AttributeError, KeyError, IndexError):
134
+ yield from self._get_values(val, rest, below)
135
+ except (StopIteration, TypeError, AttributeError, KeyError, IndexError):
128
136
  yield None, False
129
137
  return
130
138
  # Key-like part (Word, Wildcard, etc.)
131
139
  if not hasattr(node, 'keys'):
132
140
  yield None, False
133
141
  return
142
+ keys = (k for m in self._key_matchers(part, node, settings) for k in m.matches(node.keys()))
134
143
  if len(parts) == 1 and not isinstance(part, Slot):
135
144
  # Simple key - yield all matching values (existing single-key behavior)
136
145
  found_any = False
137
- for km in part.matches(node.keys()):
146
+ for km in keys:
138
147
  yield node[km], True
139
148
  found_any = True
140
149
  if not found_any:
@@ -142,14 +151,31 @@ class FilterKey(base.MatchOp):
142
151
  return
143
152
  # Dotted path - traverse (first match only at each key-like level)
144
153
  found = False
145
- for km in part.matches(node.keys()):
154
+ for km in keys:
146
155
  child = node[km]
147
- yield from self._get_values(child, rest, settings)
156
+ yield from self._get_values(child, rest, below)
148
157
  found = True
149
158
  break
150
159
  if not found:
151
160
  yield None, False
152
161
 
162
+ def _key_matchers(self, part, node, settings):
163
+ """
164
+ The match ops a key-like part tests node's keys with: a reference
165
+ resolved against node and its ancestors, one per value when its
166
+ path is a pattern, none when it does not resolve; the part itself
167
+ otherwise.
168
+ """
169
+ if not part.is_reference():
170
+ return (part,)
171
+ try:
172
+ val = settings.resolve(part, node, settings.parents or ())
173
+ except KeyError:
174
+ return ()
175
+ if part.is_pattern():
176
+ return tuple(matchers.ResolvedValue(v) for v in val)
177
+ return (matchers.ResolvedValue(val),)
178
+
153
179
  def get_value(self, node):
154
180
  """
155
181
  Get first matching value from node (backwards compat)
@@ -285,7 +311,7 @@ class FilterKeyValue(FilterOp):
285
311
  val = results.apply_transforms(val, self.guard_transforms, settings, parents)
286
312
  except base.UnresolvedReference:
287
313
  continue
288
- if results.guard_passes(pred_op, val, self.val, settings, val, parents):
314
+ if results.guard_passes(pred_op, val, self.val, settings, val, parents, 'filters'):
289
315
  return True
290
316
  return False
291
317
 
@@ -260,8 +260,11 @@ regex = _regex.copy().set_parse_action(matchers.Regex)
260
260
  regex_first = (_regex + pp.Suppress(pp.Literal('?'))).set_parse_action(matchers.RegexFirst)
261
261
  _escaped_dollar = pp.Regex(r'\\\$\${0,2}(\([^)]*\)|[0-9]*)').set_parse_action(
262
262
  lambda t: matchers.Word(t[0][1:]))
263
- _reference = pp.Regex(r'\$\$\([^)]+\)').set_parse_action(
264
- lambda t: matchers.Reference(t[0][3:-1]))
263
+ # $$(path) runs to the ) that closes it, so the path may hold parentheses
264
+ # of its own, $$((a[*];)) or $$(a.(b,c)); a ) inside quotes does not count.
265
+ _reference = (S(pp.Regex(r'\$\$(?=\()'))
266
+ + pp.original_text_for(pp.nested_expr('(', ')')).add_condition(lambda t: t[0] != '()')
267
+ ).set_parse_action(lambda t: matchers.Reference(t[0][1:-1]))
265
268
  def _paren_subst_action(t):
266
269
  """
267
270
  Parse $(content) where content is name_or_index[|transform1|transform2...].
@@ -285,8 +288,11 @@ var = _escaped_dollar | _reference | _paren_subst | _raw_subst | _dollar_word
285
288
  # |min:$amount.value or =^lots.taken: one string, for a resolver to read
286
289
  # whole. In a segment position the . separates segments, so not there.
287
290
  _arg_sigil = pp.Regex(r'(?:\$(?![0-9($])|\^)[^' + re.escape(reserved.replace('.', '')) + r' \t\n\r]+').set_parse_action(matchers.Word)
288
- slice = pp.Optional(integer | plus) + ':' + pp.Optional(integer | plus) \
289
- + pp.Optional(':') + pp.Optional(integer | plus)
291
+ # A slice bound: an integer, + (the length), or a $$(path) reference or
292
+ # $(name)/$N substitution standing for one.
293
+ _slice_bound = integer | plus | _reference | _paren_subst | _raw_subst
294
+ slice = pp.Optional(_slice_bound) + ':' + pp.Optional(_slice_bound) \
295
+ + pp.Optional(':') + pp.Optional(_slice_bound)
290
296
 
291
297
  _common_pats = wildcard_first | wildcard | var | regex_first | regex
292
298
  _commons = bytes_literal | string | _common_pats | numeric_quoted
@@ -92,9 +92,33 @@ class Recursive(BaseOp):
92
92
  s = '.' + s
93
93
  return s
94
94
 
95
+ def _parts(self):
96
+ """
97
+ The inner match op, the accessor ops and the filters: every part
98
+ that may hold a substitution or reference.
99
+ """
100
+ yield self.inner
101
+ for item in self.accessors or ():
102
+ if item is base.BRANCH_CUT or item is base.BRANCH_SOFTCUT:
103
+ continue
104
+ yield from item
105
+ yield from self.filters
106
+
107
+ def is_template(self):
108
+ """
109
+ True if a part contains a substitution reference.
110
+ """
111
+ return any(hasattr(p, 'is_template') and p.is_template() for p in self._parts())
112
+
113
+ def is_reference(self):
114
+ """
115
+ True if a part contains an internal reference.
116
+ """
117
+ return any(hasattr(p, 'is_reference') and p.is_reference() for p in self._parts())
118
+
95
119
  def resolve(self, bindings, partial=False):
96
120
  """
97
- Resolve $N in inner match op and accessor branches.
121
+ Resolve $N in inner match op, accessor branches and filters.
98
122
  """
99
123
  new_inner = self.inner.resolve(bindings, partial) if hasattr(self.inner, 'resolve') else self.inner
100
124
  new_accessors = None
@@ -111,11 +135,16 @@ class Recursive(BaseOp):
111
135
  acc_changed = True
112
136
  new_accessors.append(new_branch)
113
137
  new_accessors = tuple(new_accessors)
114
- if new_inner is self.inner and not acc_changed:
138
+ new_filters = tuple(f.resolve(bindings, partial) for f in self.filters)
139
+ filt_changed = not all(nf is of for nf, of in zip(new_filters, self.filters))
140
+ if new_inner is self.inner and not acc_changed and not filt_changed:
115
141
  return self
116
- return Recursive(
142
+ # type(self): a RecursiveFirst stays first-match
143
+ r = type(self)(
117
144
  new_inner, accessors=new_accessors if new_accessors is not None else self.accessors,
118
145
  depth_start=self.depth_start, depth_stop=self.depth_stop, depth_step=self.depth_step)
146
+ r.filters = new_filters
147
+ return r
119
148
 
120
149
  def match(self, op, specials=False):
121
150
  return self.inner.matchable(op, specials=specials)
@@ -256,33 +256,41 @@ def apply_transforms(val, transforms, settings=base.SETTINGS, parents=()):
256
256
  return val
257
257
 
258
258
 
259
- def guard_matcher(guard, settings=base.SETTINGS, node=None, parents=()):
259
+ def guard_matcher(guard, settings=base.SETTINGS, node=None, parents=(), position='guards'):
260
260
  """
261
261
  The match op a guard or filter value compares with: the value as
262
262
  parsed, or a reference resolved through the traversal's resolver
263
263
  against node and parents, node being the value under test (so
264
- $$(^x) looks x up in it). A reference path that is a pattern becomes
265
- a value group of what it resolves to. Raises UnresolvedReference when
266
- the reference does not resolve.
264
+ $$(^x) looks x up in it). A plain value is resolved too when the
265
+ resolver takes literals at position, 'guards' or 'filters'. A reference
266
+ path that is a pattern becomes a value group of what it resolves to.
267
+ A container value resolves the references inside it the same way; one
268
+ that is a pattern branches the container, so it becomes a value group
269
+ of the container once per value (see containers.ContainerOp).
270
+ Raises UnresolvedReference when the reference does not resolve.
267
271
  """
268
- # container values (ContainerList and kin) are ops without is_reference
269
272
  reference = hasattr(guard, 'is_reference') and guard.is_reference()
270
- if not (reference or (settings.literals() and isinstance(guard, Const))):
273
+ if not (reference or (settings.literals(position) and isinstance(guard, Const))):
271
274
  return guard
275
+ from . import containers
276
+ if isinstance(guard, containers.ContainerOp):
277
+ branches = guard.branches(settings, node, parents)
278
+ if len(branches) == 1:
279
+ return branches[0]
280
+ return containers.ValueGroup(*branches)
272
281
  val = settings.resolve(guard, node, parents)
273
282
  if not guard.is_pattern():
274
283
  return Const(val)
275
- from . import containers
276
284
  return containers.ValueGroup(*(Const(v) for v in val))
277
285
 
278
286
 
279
- def guard_passes(pred_op, val, guard, settings=base.SETTINGS, node=None, parents=()):
287
+ def guard_passes(pred_op, val, guard, settings=base.SETTINGS, node=None, parents=(), position='guards'):
280
288
  """
281
289
  True if val satisfies pred_op against guard (see guard_matcher); a
282
290
  guard reference that does not resolve passes nothing.
283
291
  """
284
292
  try:
285
- matcher = guard_matcher(guard, settings, node, parents)
293
+ matcher = guard_matcher(guard, settings, node, parents, position)
286
294
  except base.UnresolvedReference:
287
295
  return False
288
296
  return any(True for _ in pred_op.matches((val,), matcher))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.49.1
3
+ Version: 0.49.3
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-Expression: MIT
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: 3.15
24
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
26
  Classifier: Topic :: Utilities
26
27
  Requires-Python: >=3.6
@@ -1163,6 +1164,14 @@ Dotted slicing works like python slicing and all that entails.
1163
1164
  >>> dotted.get(d, '*.there[1:]')
1164
1165
  ([2, 3], [5, 6])
1165
1166
 
1167
+ A bound may be a reference or a substitution, resolved the way a slot's is:
1168
+
1169
+ >>> d = {'b': [3, 0, 5], 'i': 2}
1170
+ >>> dotted.get(d, 'b[:$$(i)]')
1171
+ [3, 0]
1172
+ >>> dotted.get(d, 'b[$(n):]', bindings={'n': 1})
1173
+ [0, 5]
1174
+
1166
1175
  <a id="dot-notation-for-sequence-indexing"></a>
1167
1176
  ### Dot notation for sequence indexing
1168
1177
 
@@ -1641,6 +1650,23 @@ as do the transforms before a guard:
1641
1650
  >>> dotted.get(data, '*|mul:$$(scale)=20')
1642
1651
  (2,)
1643
1652
 
1653
+ So does a reference inside a container value. One whose path is a pattern
1654
+ branches the container, once per value; pack it, `(…;)`, to compare with the
1655
+ tuple instead:
1656
+
1657
+ >>> data = {'want': [2, 3], 'items': [[1, 2], [1, 4], [1, (2, 3)]]}
1658
+ >>> dotted.get(data, 'items[*]=[1, $$(want[*])]')
1659
+ ([1, 2],)
1660
+ >>> dotted.get(data, 'items[*]=[1, $$((want[*];))]')
1661
+ ([1, (2, 3)],)
1662
+
1663
+ A filter key may hold references too. In each part of the key, `^` is the
1664
+ node that part is looked up in, so in the first part it is the item:
1665
+
1666
+ >>> data = {'field': 'name', 'users': [{'name': 'alice'}, {'name': 'bob'}]}
1667
+ >>> dotted.get(data, 'users[*&$$(field)="bob"]')
1668
+ ({'name': 'bob'},)
1669
+
1644
1670
  With `update`, the transforms apply to the value being set, so `^` is that
1645
1671
  value:
1646
1672
 
@@ -1677,14 +1703,16 @@ claim some references and leave the rest:
1677
1703
  >>> dotted.get({'amount': 10}, 'amount|sub:$$(lots.taken)', resolver=Store(store))
1678
1704
  7
1679
1705
 
1680
- A resolver that sets `literals = True` is also offered every plain token in
1681
- those positions, so a host sigil such as `$sel` can be a variable. Return
1706
+ A resolver with `literals = True` is also offered every plain token in
1707
+ those positions, so a host sigil such as `$sel` can be a variable. Set it on
1708
+ the class, or per instance with `literals=` on the constructor. Return
1682
1709
  `ref.value` to leave a token the literal it is; a key the resolver answers
1683
1710
  is not offered again:
1684
1711
 
1685
1712
  >>> class Sigils(dotted.ReferenceResolver):
1686
1713
  ... literals = True
1687
- ... def __init__(self, names):
1714
+ ... def __init__(self, names, literals=None):
1715
+ ... super().__init__(literals)
1688
1716
  ... self.names = names
1689
1717
  ... def resolve(self, ref, settings, node=None, parents=()):
1690
1718
  ... if ref.is_reference():
@@ -1696,6 +1724,18 @@ is not offered again:
1696
1724
  >>> dotted.get(users, 'users[*&name=$sel].name', resolver=Sigils({'$sel': 'bob'}))
1697
1725
  ('bob',)
1698
1726
 
1727
+ `literals` may instead name the positions it wants, as a list or tuple of
1728
+ `'segments'` (key, slot and attr segments), `'transforms'` (transform
1729
+ arguments), `'guards'` (guard values) and `'filters'` (filter values). A host
1730
+ whose variables only ever stand as arguments leaves `'segments'` out, so the
1731
+ keys of a path cost no call:
1732
+
1733
+ >>> r = Sigils({'$inc': 3}, literals=('transforms', 'guards', 'filters'))
1734
+ >>> dotted.get({'n': 5}, 'n|add:$inc', resolver=r)
1735
+ 8
1736
+
1737
+ An unknown name is a `ValueError`, on the class or the constructor.
1738
+
1699
1739
  A host variable may span several segments, `$amount.value`, which dotted has
1700
1740
  parsed as two. The path-level hook `fold(segments)` is called once with a
1701
1741
  path's segment strings, `('$amount', 'value')`, and returns the runs it
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "dotted_notation"
9
- version = "0.49.1"
9
+ version = "0.49.3"
10
10
  description = "Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.6"
@@ -38,6 +38,7 @@ classifiers = [
38
38
  "Programming Language :: Python :: 3.12",
39
39
  "Programming Language :: Python :: 3.13",
40
40
  "Programming Language :: Python :: 3.14",
41
+ "Programming Language :: Python :: 3.15",
41
42
  "Topic :: Software Development :: Libraries :: Python Modules",
42
43
  "Topic :: Utilities",
43
44
  ]
@@ -78,7 +79,7 @@ Changelog = "https://github.com/freywaid/dotted/blob/master/CHANGELOG.md"
78
79
  include = ["dotted*"]
79
80
 
80
81
  [tool.cibuildwheel]
81
- # Compiled wheels for CPython 3.11-3.14 (the versions are listed in the
82
+ # Compiled wheels for CPython 3.11-3.15 (the versions are listed in the
82
83
  # publish workflow, one job each); everything else installs the pure wheel.
83
84
  skip = "pp*"
84
85
  test-requires = "pytest"
@@ -64,4 +64,7 @@ class build_ext(_build_ext):
64
64
  self.parallel = os.cpu_count() or 1
65
65
 
66
66
 
67
- setup(ext_modules=extensions(), cmdclass={'build_ext': build_ext})
67
+ # Guarded: cythonize compiles in a process pool, and a spawned worker
68
+ # (macOS, and Linux from Python 3.14) imports this file again.
69
+ if __name__ == '__main__':
70
+ setup(ext_modules=extensions(), cmdclass={'build_ext': build_ext})