dotted-notation 0.47.2__tar.gz → 0.48.0__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.47.2 → dotted_notation-0.48.0}/CHANGELOG.md +19 -0
  2. {dotted_notation-0.47.2/dotted_notation.egg-info → dotted_notation-0.48.0}/PKG-INFO +67 -1
  3. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/README.md +66 -0
  4. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/__init__.py +4 -1
  5. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/access.py +20 -10
  6. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/api.py +123 -70
  7. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/base.pxd +1 -0
  8. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/base.py +99 -13
  9. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/engine.pxd +4 -4
  10. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/engine.py +18 -17
  11. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/filters.py +2 -2
  12. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/matchers.py +7 -31
  13. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/results.py +83 -18
  14. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/wrappers.py +30 -3
  15. {dotted_notation-0.47.2 → dotted_notation-0.48.0/dotted_notation.egg-info}/PKG-INFO +67 -1
  16. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/pyproject.toml +1 -1
  17. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/LICENSE +0 -0
  18. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/MANIFEST.in +0 -0
  19. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/__main__.py +0 -0
  20. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/access.pxd +0 -0
  21. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/cli/__init__.py +0 -0
  22. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/cli/_compat.py +0 -0
  23. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/cli/formats.py +0 -0
  24. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/cli/main.py +0 -0
  25. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/containers.py +0 -0
  26. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/filters.pxd +0 -0
  27. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/grammar.py +0 -0
  28. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/groups.pxd +0 -0
  29. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/groups.py +0 -0
  30. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/matchers.pxd +0 -0
  31. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/native.py +0 -0
  32. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/predicates.py +0 -0
  33. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/recursive.pxd +0 -0
  34. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/recursive.py +0 -0
  35. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/sql/__init__.py +0 -0
  36. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/sql/core.py +0 -0
  37. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/sql/pg.py +0 -0
  38. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/transforms.py +0 -0
  39. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/utils.py +0 -0
  40. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/utypes.py +0 -0
  41. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted/wrappers.pxd +0 -0
  42. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted_notation.egg-info/SOURCES.txt +0 -0
  43. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted_notation.egg-info/dependency_links.txt +0 -0
  44. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted_notation.egg-info/entry_points.txt +0 -0
  45. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted_notation.egg-info/requires.txt +0 -0
  46. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/dotted_notation.egg-info/top_level.txt +0 -0
  47. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/setup.cfg +0 -0
  48. {dotted_notation-0.47.2 → dotted_notation-0.48.0}/setup.py +0 -0
@@ -3,6 +3,25 @@
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.48.0]
7
+
8
+ ### Added
9
+ - `dotted.ReferenceResolver`: what a reference means at run time, as a
10
+ class a host subclasses. `resolve(ref, settings, node, parents)` is
11
+ called for every `$$(path)` a traversal meets, in an access position,
12
+ a transform argument, a guard or a filter value; `super().resolve` is
13
+ dotted's own reading. With `literals = True` it is offered every plain
14
+ token in those positions too, so a host sigil such as `$sel` can be a
15
+ variable. `fold(segments)` is called once per path with its segment
16
+ strings and folds runs of them into pre-resolved keys, so a
17
+ multi-segment host variable, `$amount.value`, is one name. Pass a
18
+ resolver as `resolver=` to `get`, `update`, `remove` and the other
19
+ traversal calls, or install one for the process with
20
+ `dotted.set_resolver(r)`; `get_resolver()` reads it back.
21
+ `UnresolvedReference` is exported for resolvers to raise.
22
+ - `Dotted.segments()`, each segment's plain value with its wrappers
23
+ aside, and slicing a parsed path, `ops[1:]`, gives a path.
24
+
6
25
  ## [0.47.2]
7
26
 
8
27
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.47.2
3
+ Version: 0.48.0
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
@@ -145,6 +145,7 @@ For faster `mutable=False` updates and removes, install the
145
145
  - [References](#references)
146
146
  - [Relative References](#relative-references)
147
147
  - [References in transforms and guards](#references-in-transforms-and-guards)
148
+ - [Custom resolvers](#custom-resolvers)
148
149
  - [Escaping](#escaping)
149
150
  - [Type Restrictions](#type-restrictions)
150
151
  - [Positive restrictions](#positive-restrictions)
@@ -1631,6 +1632,71 @@ A reference that does not resolve matches nothing, as in an access position:
1631
1632
  >>> dotted.get({'a': 1}, 'a|add:$$(missing)', default='fallback')
1632
1633
  'fallback'
1633
1634
 
1635
+ <a id="custom-resolvers"></a>
1636
+ ### Custom resolvers
1637
+
1638
+ What a reference means at run time is decided by a **resolver**. The default
1639
+ is dotted's own reading above. A host with its own namespaces subclasses
1640
+ `dotted.ReferenceResolver`, passes an instance as `resolver=` to `get`,
1641
+ `update`, `remove` and the rest, or installs one for the process with
1642
+ `dotted.set_resolver(r)`; `set_resolver()` with no argument restores the
1643
+ default.
1644
+
1645
+ `resolve(ref, settings, node, parents)` is called for every `$$(...)` the
1646
+ traversal meets, in an access position, a transform argument, a guard or a
1647
+ filter value. `ref.is_reference()` is true for those, `ref.inner_path` is the
1648
+ text inside, and `super().resolve(...)` is dotted's reading, so a host can
1649
+ claim some references and leave the rest:
1650
+
1651
+ >>> class Store(dotted.ReferenceResolver):
1652
+ ... def __init__(self, store):
1653
+ ... self.store = store
1654
+ ... def resolve(self, ref, settings, node=None, parents=()):
1655
+ ... return dotted.get(self.store, ref.inner_path)
1656
+ >>> store = {'lots': {'taken': 3}}
1657
+ >>> dotted.get({'amount': 10}, 'amount|sub:$$(lots.taken)', resolver=Store(store))
1658
+ 7
1659
+
1660
+ A resolver that sets `literals = True` is also offered every plain token in
1661
+ those positions, so a host sigil such as `$sel` can be a variable. Return
1662
+ `ref.value` to leave a token the literal it is; a key the resolver answers
1663
+ is not offered again:
1664
+
1665
+ >>> class Sigils(dotted.ReferenceResolver):
1666
+ ... literals = True
1667
+ ... def __init__(self, names):
1668
+ ... self.names = names
1669
+ ... def resolve(self, ref, settings, node=None, parents=()):
1670
+ ... if ref.is_reference():
1671
+ ... return super().resolve(ref, settings, node, parents)
1672
+ ... if isinstance(ref.value, str) and ref.value.startswith('$'):
1673
+ ... return self.names[ref.value]
1674
+ ... return ref.value
1675
+ >>> users = {'users': [{'name': 'alice'}, {'name': 'bob'}]}
1676
+ >>> dotted.get(users, 'users[*&name=$sel].name', resolver=Sigils({'$sel': 'bob'}))
1677
+ ('bob',)
1678
+
1679
+ A host variable may span several segments, `$amount.value`, which dotted has
1680
+ parsed as two. The path-level hook `fold(segments)` is called once with a
1681
+ path's segment strings, `('$amount', 'value')`, and returns the runs it
1682
+ resolves as `(start, stop, value)` triples; each run becomes one segment
1683
+ whose key is the value, keeping the last segment's guard, filter and
1684
+ transforms. An inner segment of a run may carry none.
1685
+
1686
+ >>> class Folding(Sigils):
1687
+ ... def fold(self, segments):
1688
+ ... name = '.'.join(str(s) for s in segments)
1689
+ ... if name in self.names:
1690
+ ... return [(0, len(segments), self.names[name])]
1691
+ ... return []
1692
+ >>> r = Folding({'$amount.value': 'amt'})
1693
+ >>> dotted.get({'amt': 10}, '$amount.value|sub:3', resolver=r)
1694
+ 7
1695
+
1696
+ A path handed in already parsed goes through `fold()` again, so a hook must
1697
+ be content to see its own output. `Dotted.segments()` gives any parsed
1698
+ path's segment values, and a slice of a parsed path, `ops[1:]`, is a path.
1699
+
1634
1700
  <a id="escaping"></a>
1635
1701
  ### Escaping
1636
1702
 
@@ -98,6 +98,7 @@ For faster `mutable=False` updates and removes, install the
98
98
  - [References](#references)
99
99
  - [Relative References](#relative-references)
100
100
  - [References in transforms and guards](#references-in-transforms-and-guards)
101
+ - [Custom resolvers](#custom-resolvers)
101
102
  - [Escaping](#escaping)
102
103
  - [Type Restrictions](#type-restrictions)
103
104
  - [Positive restrictions](#positive-restrictions)
@@ -1584,6 +1585,71 @@ A reference that does not resolve matches nothing, as in an access position:
1584
1585
  >>> dotted.get({'a': 1}, 'a|add:$$(missing)', default='fallback')
1585
1586
  'fallback'
1586
1587
 
1588
+ <a id="custom-resolvers"></a>
1589
+ ### Custom resolvers
1590
+
1591
+ What a reference means at run time is decided by a **resolver**. The default
1592
+ is dotted's own reading above. A host with its own namespaces subclasses
1593
+ `dotted.ReferenceResolver`, passes an instance as `resolver=` to `get`,
1594
+ `update`, `remove` and the rest, or installs one for the process with
1595
+ `dotted.set_resolver(r)`; `set_resolver()` with no argument restores the
1596
+ default.
1597
+
1598
+ `resolve(ref, settings, node, parents)` is called for every `$$(...)` the
1599
+ traversal meets, in an access position, a transform argument, a guard or a
1600
+ filter value. `ref.is_reference()` is true for those, `ref.inner_path` is the
1601
+ text inside, and `super().resolve(...)` is dotted's reading, so a host can
1602
+ claim some references and leave the rest:
1603
+
1604
+ >>> class Store(dotted.ReferenceResolver):
1605
+ ... def __init__(self, store):
1606
+ ... self.store = store
1607
+ ... def resolve(self, ref, settings, node=None, parents=()):
1608
+ ... return dotted.get(self.store, ref.inner_path)
1609
+ >>> store = {'lots': {'taken': 3}}
1610
+ >>> dotted.get({'amount': 10}, 'amount|sub:$$(lots.taken)', resolver=Store(store))
1611
+ 7
1612
+
1613
+ A resolver that sets `literals = True` is also offered every plain token in
1614
+ those positions, so a host sigil such as `$sel` can be a variable. Return
1615
+ `ref.value` to leave a token the literal it is; a key the resolver answers
1616
+ is not offered again:
1617
+
1618
+ >>> class Sigils(dotted.ReferenceResolver):
1619
+ ... literals = True
1620
+ ... def __init__(self, names):
1621
+ ... self.names = names
1622
+ ... def resolve(self, ref, settings, node=None, parents=()):
1623
+ ... if ref.is_reference():
1624
+ ... return super().resolve(ref, settings, node, parents)
1625
+ ... if isinstance(ref.value, str) and ref.value.startswith('$'):
1626
+ ... return self.names[ref.value]
1627
+ ... return ref.value
1628
+ >>> users = {'users': [{'name': 'alice'}, {'name': 'bob'}]}
1629
+ >>> dotted.get(users, 'users[*&name=$sel].name', resolver=Sigils({'$sel': 'bob'}))
1630
+ ('bob',)
1631
+
1632
+ A host variable may span several segments, `$amount.value`, which dotted has
1633
+ parsed as two. The path-level hook `fold(segments)` is called once with a
1634
+ path's segment strings, `('$amount', 'value')`, and returns the runs it
1635
+ resolves as `(start, stop, value)` triples; each run becomes one segment
1636
+ whose key is the value, keeping the last segment's guard, filter and
1637
+ transforms. An inner segment of a run may carry none.
1638
+
1639
+ >>> class Folding(Sigils):
1640
+ ... def fold(self, segments):
1641
+ ... name = '.'.join(str(s) for s in segments)
1642
+ ... if name in self.names:
1643
+ ... return [(0, len(segments), self.names[name])]
1644
+ ... return []
1645
+ >>> r = Folding({'$amount.value': 'amt'})
1646
+ >>> dotted.get({'amt': 10}, '$amount.value|sub:3', resolver=r)
1647
+ 7
1648
+
1649
+ A path handed in already parsed goes through `fold()` again, so a hook must
1650
+ be content to see its own output. `Dotted.segments()` gives any parsed
1651
+ path's segment values, and a slice of a parsed path, `ops[1:]`, is a path.
1652
+
1587
1653
  <a id="escaping"></a>
1588
1654
  ### Escaping
1589
1655
 
@@ -65,7 +65,7 @@ from .api import \
65
65
  parse, compile, is_pattern, is_template, is_reference, is_indeterminate, is_simple, \
66
66
  is_inverted, is_mutable, mutable, quote, ANY, AUTO, Attrs, GroupMode, \
67
67
  set_simple_fastpath, set_parse_cache, \
68
- register, transform, \
68
+ register, transform, set_resolver, get_resolver, \
69
69
  assemble, assemble_multi, \
70
70
  build, build_multi, \
71
71
  expand, expand_multi, \
@@ -77,10 +77,13 @@ from .api import \
77
77
  remove, remove_multi, remove_if, remove_if_multi, \
78
78
  pluck, pluck_multi, walk, walk_multi, unpack, keys, values, items
79
79
  from .sql import sqlize, TranslationError, SQLFragment, Resolver, ParamStyle, ParamPool
80
+ # the run-time reference resolver; sqlize's Resolver, above, has the plain name
81
+ from .base import Resolver as ReferenceResolver, UnresolvedReference
80
82
 
81
83
  __all__ = [
82
84
  # Core
83
85
  'get', 'update', 'remove', 'has', 'setdefault',
86
+ 'ReferenceResolver', 'UnresolvedReference', 'set_resolver', 'get_resolver',
84
87
  # Multi
85
88
  'get_multi', 'update_multi', 'pack', 'update_if', 'update_if_multi', 'remove_multi', 'remove_if', 'remove_if_multi', 'setdefault_multi',
86
89
  # Pattern
@@ -248,6 +248,16 @@ class AccessOp(SimpleOp):
248
248
  """
249
249
  return self.op.is_reference()
250
250
 
251
+ def _offered(self, settings):
252
+ """
253
+ True if the match op goes to the resolver: a reference, or a
254
+ plain constant when the resolver in force takes literals.
255
+ """
256
+ if self.op.is_reference():
257
+ return True
258
+ return (settings.literals() and isinstance(self.op, matchers.Const)
259
+ and not isinstance(self.op, matchers.ResolvedValue))
260
+
251
261
  def resolve(self, bindings, partial=False):
252
262
  """
253
263
  Resolve $N in the inner match op.
@@ -263,21 +273,21 @@ class AccessOp(SimpleOp):
263
273
  tuple of concrete ops (empty if the reference path is not found).
264
274
  Callers must guard with is_reference() before calling.
265
275
  """
266
- root = settings.root
267
- parents = settings.parents or ()
268
276
  try:
269
- val = self.op.resolve_ref(root, node=node, parents=parents)
277
+ val = settings.resolve(self.op, node, settings.parents or ())
270
278
  except KeyError:
271
279
  return ()
280
+ # ResolvedValue, not Const: a resolved key is not offered to the
281
+ # resolver again (see _offered)
272
282
  if self.op.is_pattern():
273
- return tuple(self.__class__(matchers.Const(v)) for v in val)
274
- return (self.__class__(matchers.Const(val)),)
283
+ return tuple(self.__class__(matchers.ResolvedValue(v)) for v in val)
284
+ return (self.__class__(matchers.ResolvedValue(val)),)
275
285
 
276
286
  def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, settings=base.SETTINGS):
277
287
  """
278
288
  Resolve references before updating.
279
289
  """
280
- if self.is_reference():
290
+ if self._offered(settings):
281
291
  for r in self._resolved(node=node, settings=settings):
282
292
  node = r.do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap, settings=settings)
283
293
  return node
@@ -287,7 +297,7 @@ class AccessOp(SimpleOp):
287
297
  """
288
298
  Resolve references before removing.
289
299
  """
290
- if self.is_reference():
300
+ if self._offered(settings):
291
301
  for r in self._resolved(node=node, settings=settings):
292
302
  node = r.do_remove(ops, node, val, nop, settings=settings)
293
303
  return node
@@ -395,7 +405,7 @@ class Key(AccessOp):
395
405
  return iter(pairs)
396
406
 
397
407
  def items(self, node, settings=base.SETTINGS, filtered=True):
398
- if self.is_reference():
408
+ if self._offered(settings):
399
409
  return self._reference_items(node, filtered, settings)
400
410
  # Dict-like: use key matching
401
411
  if hasattr(node, 'keys'):
@@ -586,7 +596,7 @@ class Attr(Key):
586
596
  yield (key, v)
587
597
 
588
598
  def items(self, node, settings=base.SETTINGS, filtered=True):
589
- if self.is_reference():
599
+ if self._offered(settings):
590
600
  return self._reference_items(node, filtered, settings)
591
601
  all_keys = self._all_keys(node)
592
602
  keys = self.op.matches(all_keys) if filtered else all_keys
@@ -673,7 +683,7 @@ class Slot(Key):
673
683
  return '[' + self.op.quote() + ']'
674
684
 
675
685
  def items(self, node, settings=base.SETTINGS, filtered=True):
676
- if self.is_reference():
686
+ if self._offered(settings):
677
687
  return self._reference_items(node, filtered, settings)
678
688
  if hasattr(node, 'keys'):
679
689
  if settings.strict: