dotted-notation 0.49.0__tar.gz → 0.49.2__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.0 → dotted_notation-0.49.2}/CHANGELOG.md +24 -0
  2. {dotted_notation-0.49.0/dotted_notation.egg-info → dotted_notation-0.49.2}/PKG-INFO +37 -8
  3. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/README.md +36 -7
  4. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/access.py +3 -2
  5. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/api.py +5 -4
  6. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/base.py +60 -6
  7. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/filters.py +1 -1
  8. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/results.py +8 -7
  9. {dotted_notation-0.49.0 → dotted_notation-0.49.2/dotted_notation.egg-info}/PKG-INFO +37 -8
  10. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/pyproject.toml +1 -1
  11. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/setup.py +15 -1
  12. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/LICENSE +0 -0
  13. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/MANIFEST.in +0 -0
  14. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/__init__.py +0 -0
  15. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/__main__.py +0 -0
  16. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/access.pxd +0 -0
  17. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/base.pxd +0 -0
  18. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/cli/__init__.py +0 -0
  19. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/cli/_compat.py +0 -0
  20. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/cli/formats.py +0 -0
  21. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/cli/main.py +0 -0
  22. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/containers.py +0 -0
  23. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/engine.pxd +0 -0
  24. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/engine.py +0 -0
  25. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/filters.pxd +0 -0
  26. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/grammar.py +0 -0
  27. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/groups.pxd +0 -0
  28. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/groups.py +0 -0
  29. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/matchers.pxd +0 -0
  30. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/matchers.py +0 -0
  31. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/native.py +0 -0
  32. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/predicates.py +0 -0
  33. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/recursive.pxd +0 -0
  34. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/recursive.py +0 -0
  35. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/sql/__init__.py +0 -0
  36. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/sql/core.py +0 -0
  37. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/sql/pg.py +0 -0
  38. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/transforms.py +0 -0
  39. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/utils.py +0 -0
  40. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/utypes.py +0 -0
  41. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/wrappers.pxd +0 -0
  42. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted/wrappers.py +0 -0
  43. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted_notation.egg-info/SOURCES.txt +0 -0
  44. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted_notation.egg-info/dependency_links.txt +0 -0
  45. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted_notation.egg-info/entry_points.txt +0 -0
  46. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted_notation.egg-info/requires.txt +0 -0
  47. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/dotted_notation.egg-info/top_level.txt +0 -0
  48. {dotted_notation-0.49.0 → dotted_notation-0.49.2}/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.49.2]
7
+
8
+ ### Added
9
+ - `ReferenceResolver.literals` may name the positions a resolver is
10
+ offered plain tokens in, as a list or tuple of `'segments'`,
11
+ `'transforms'`, `'guards'` and `'filters'`. A host whose variables are
12
+ sigiled arguments, `|add:^fee`, leaves `'segments'` out and no longer
13
+ pays a call per key (#63); `get` then keeps its fast path for a plain
14
+ chain of keys. An unknown name is a `ValueError`. `True` and `False`
15
+ are unchanged.
16
+ - `ReferenceResolver(literals=...)` sets `literals` per instance,
17
+ overriding the class's, so a resolver need not be subclassed only to
18
+ change it. A subclass with its own `__init__` passes it on with
19
+ `super().__init__(literals)`.
20
+
21
+ ## [0.49.1]
22
+
23
+ ### Documentation
24
+ - The README describes a reference, `$$(expr)`, as an inline evaluation
25
+ expression: the dotted expression inside, transforms included, is
26
+ evaluated during traversal and its value stands where it is written. In a
27
+ transform argument, guard or filter value that is the value itself; in an
28
+ access position it is the key looked up.
29
+
6
30
  ## [0.49.0]
7
31
 
8
32
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.49.0
3
+ Version: 0.49.2
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
@@ -1396,7 +1396,8 @@ To chain through the items, use a pattern instead:
1396
1396
  ## Substitutions and References
1397
1397
 
1398
1398
  All `$`-prefixed syntax falls into two categories: **substitutions** (resolved
1399
- at replace time) and **references** (resolved during traversal).
1399
+ at replace time) and **references**, inline expressions evaluated during
1400
+ traversal.
1400
1401
 
1401
1402
  | Syntax | Type | Resolved against |
1402
1403
  |---|---|---|
@@ -1502,14 +1503,22 @@ the parsed result, so a path used many times can be compiled once:
1502
1503
  <a id="references"></a>
1503
1504
  ### References
1504
1505
 
1505
- A **reference** resolves a dotted path against the root object during traversal
1506
- and uses the result as a literal key. The syntax is `$$(path)`:
1506
+ A **reference**, `$$(expr)`, is an inline evaluation expression. The dotted
1507
+ expression inside is evaluated during traversal, as `get` would evaluate it
1508
+ against the root, and its value stands where the reference is written. The
1509
+ expression is a full dotted path, so it may carry patterns and transforms:
1510
+ `$$(config.offset|mul:10)` is the value of `config.offset`, times ten.
1511
+
1512
+ Where the reference stands decides what its value does. In a transform
1513
+ argument, a guard value or a filter value it is the value itself (see
1514
+ [References in transforms and guards](#references-in-transforms-and-guards)).
1515
+ In an access position, as a key or slot, it is the key to look up:
1507
1516
 
1508
1517
  >>> data = {'config': {'field': 'name'}, 'name': 'Alice'}
1509
1518
  >>> dotted.get(data, '$$(config.field)')
1510
1519
  'Alice'
1511
1520
 
1512
- Here `$$(config.field)` resolves to `'name'`, then looks up `data['name']`.
1521
+ Here `$$(config.field)` evaluates to `'name'`, then looks up `data['name']`.
1513
1522
 
1514
1523
  References work with all access types and in any position:
1515
1524
 
@@ -1606,6 +1615,12 @@ and so on:
1606
1615
  >>> dotted.get(data, 'n|add:$$(config.offset)')
1607
1616
  15
1608
1617
 
1618
+ The expression inside may chain transforms of its own; they apply to its value
1619
+ before it is passed:
1620
+
1621
+ >>> dotted.get(data, 'n|add:$$(config.offset|mul:10)')
1622
+ 105
1623
+
1609
1624
  >>> data = {'items': [{'price': 100, 'rate': 1.5}, {'price': 20, 'rate': 2}]}
1610
1625
  >>> dotted.get(data, 'items[*].price|mul:$$(^^rate)')
1611
1626
  (150.0, 40)
@@ -1662,14 +1677,16 @@ claim some references and leave the rest:
1662
1677
  >>> dotted.get({'amount': 10}, 'amount|sub:$$(lots.taken)', resolver=Store(store))
1663
1678
  7
1664
1679
 
1665
- A resolver that sets `literals = True` is also offered every plain token in
1666
- those positions, so a host sigil such as `$sel` can be a variable. Return
1680
+ A resolver with `literals = True` is also offered every plain token in
1681
+ those positions, so a host sigil such as `$sel` can be a variable. Set it on
1682
+ the class, or per instance with `literals=` on the constructor. Return
1667
1683
  `ref.value` to leave a token the literal it is; a key the resolver answers
1668
1684
  is not offered again:
1669
1685
 
1670
1686
  >>> class Sigils(dotted.ReferenceResolver):
1671
1687
  ... literals = True
1672
- ... def __init__(self, names):
1688
+ ... def __init__(self, names, literals=None):
1689
+ ... super().__init__(literals)
1673
1690
  ... self.names = names
1674
1691
  ... def resolve(self, ref, settings, node=None, parents=()):
1675
1692
  ... if ref.is_reference():
@@ -1681,6 +1698,18 @@ is not offered again:
1681
1698
  >>> dotted.get(users, 'users[*&name=$sel].name', resolver=Sigils({'$sel': 'bob'}))
1682
1699
  ('bob',)
1683
1700
 
1701
+ `literals` may instead name the positions it wants, as a list or tuple of
1702
+ `'segments'` (key, slot and attr segments), `'transforms'` (transform
1703
+ arguments), `'guards'` (guard values) and `'filters'` (filter values). A host
1704
+ whose variables only ever stand as arguments leaves `'segments'` out, so the
1705
+ keys of a path cost no call:
1706
+
1707
+ >>> r = Sigils({'$inc': 3}, literals=('transforms', 'guards', 'filters'))
1708
+ >>> dotted.get({'n': 5}, 'n|add:$inc', resolver=r)
1709
+ 8
1710
+
1711
+ An unknown name is a `ValueError`, on the class or the constructor.
1712
+
1684
1713
  A host variable may span several segments, `$amount.value`, which dotted has
1685
1714
  parsed as two. The path-level hook `fold(segments)` is called once with a
1686
1715
  path's segment strings, `('$amount', 'value')`, and returns the runs it
@@ -1349,7 +1349,8 @@ To chain through the items, use a pattern instead:
1349
1349
  ## Substitutions and References
1350
1350
 
1351
1351
  All `$`-prefixed syntax falls into two categories: **substitutions** (resolved
1352
- at replace time) and **references** (resolved during traversal).
1352
+ at replace time) and **references**, inline expressions evaluated during
1353
+ traversal.
1353
1354
 
1354
1355
  | Syntax | Type | Resolved against |
1355
1356
  |---|---|---|
@@ -1455,14 +1456,22 @@ the parsed result, so a path used many times can be compiled once:
1455
1456
  <a id="references"></a>
1456
1457
  ### References
1457
1458
 
1458
- A **reference** resolves a dotted path against the root object during traversal
1459
- and uses the result as a literal key. The syntax is `$$(path)`:
1459
+ A **reference**, `$$(expr)`, is an inline evaluation expression. The dotted
1460
+ expression inside is evaluated during traversal, as `get` would evaluate it
1461
+ against the root, and its value stands where the reference is written. The
1462
+ expression is a full dotted path, so it may carry patterns and transforms:
1463
+ `$$(config.offset|mul:10)` is the value of `config.offset`, times ten.
1464
+
1465
+ Where the reference stands decides what its value does. In a transform
1466
+ argument, a guard value or a filter value it is the value itself (see
1467
+ [References in transforms and guards](#references-in-transforms-and-guards)).
1468
+ In an access position, as a key or slot, it is the key to look up:
1460
1469
 
1461
1470
  >>> data = {'config': {'field': 'name'}, 'name': 'Alice'}
1462
1471
  >>> dotted.get(data, '$$(config.field)')
1463
1472
  'Alice'
1464
1473
 
1465
- Here `$$(config.field)` resolves to `'name'`, then looks up `data['name']`.
1474
+ Here `$$(config.field)` evaluates to `'name'`, then looks up `data['name']`.
1466
1475
 
1467
1476
  References work with all access types and in any position:
1468
1477
 
@@ -1559,6 +1568,12 @@ and so on:
1559
1568
  >>> dotted.get(data, 'n|add:$$(config.offset)')
1560
1569
  15
1561
1570
 
1571
+ The expression inside may chain transforms of its own; they apply to its value
1572
+ before it is passed:
1573
+
1574
+ >>> dotted.get(data, 'n|add:$$(config.offset|mul:10)')
1575
+ 105
1576
+
1562
1577
  >>> data = {'items': [{'price': 100, 'rate': 1.5}, {'price': 20, 'rate': 2}]}
1563
1578
  >>> dotted.get(data, 'items[*].price|mul:$$(^^rate)')
1564
1579
  (150.0, 40)
@@ -1615,14 +1630,16 @@ claim some references and leave the rest:
1615
1630
  >>> dotted.get({'amount': 10}, 'amount|sub:$$(lots.taken)', resolver=Store(store))
1616
1631
  7
1617
1632
 
1618
- A resolver that sets `literals = True` is also offered every plain token in
1619
- those positions, so a host sigil such as `$sel` can be a variable. Return
1633
+ A resolver with `literals = True` is also offered every plain token in
1634
+ those positions, so a host sigil such as `$sel` can be a variable. Set it on
1635
+ the class, or per instance with `literals=` on the constructor. Return
1620
1636
  `ref.value` to leave a token the literal it is; a key the resolver answers
1621
1637
  is not offered again:
1622
1638
 
1623
1639
  >>> class Sigils(dotted.ReferenceResolver):
1624
1640
  ... literals = True
1625
- ... def __init__(self, names):
1641
+ ... def __init__(self, names, literals=None):
1642
+ ... super().__init__(literals)
1626
1643
  ... self.names = names
1627
1644
  ... def resolve(self, ref, settings, node=None, parents=()):
1628
1645
  ... if ref.is_reference():
@@ -1634,6 +1651,18 @@ is not offered again:
1634
1651
  >>> dotted.get(users, 'users[*&name=$sel].name', resolver=Sigils({'$sel': 'bob'}))
1635
1652
  ('bob',)
1636
1653
 
1654
+ `literals` may instead name the positions it wants, as a list or tuple of
1655
+ `'segments'` (key, slot and attr segments), `'transforms'` (transform
1656
+ arguments), `'guards'` (guard values) and `'filters'` (filter values). A host
1657
+ whose variables only ever stand as arguments leaves `'segments'` out, so the
1658
+ keys of a path cost no call:
1659
+
1660
+ >>> r = Sigils({'$inc': 3}, literals=('transforms', 'guards', 'filters'))
1661
+ >>> dotted.get({'n': 5}, 'n|add:$inc', resolver=r)
1662
+ 8
1663
+
1664
+ An unknown name is a `ValueError`, on the class or the constructor.
1665
+
1637
1666
  A host variable may span several segments, `$amount.value`, which dotted has
1638
1667
  parsed as two. The path-level hook `fold(segments)` is called once with a
1639
1668
  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):
@@ -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))):
@@ -285,7 +285,7 @@ class FilterKeyValue(FilterOp):
285
285
  val = results.apply_transforms(val, self.guard_transforms, settings, parents)
286
286
  except base.UnresolvedReference:
287
287
  continue
288
- if results.guard_passes(pred_op, val, self.val, settings, val, parents):
288
+ if results.guard_passes(pred_op, val, self.val, settings, val, parents, 'filters'):
289
289
  return True
290
290
  return False
291
291
 
@@ -256,18 +256,19 @@ 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
+ Raises UnresolvedReference when the reference does not resolve.
267
268
  """
268
269
  # container values (ContainerList and kin) are ops without is_reference
269
270
  reference = hasattr(guard, 'is_reference') and guard.is_reference()
270
- if not (reference or (settings.literals() and isinstance(guard, Const))):
271
+ if not (reference or (settings.literals(position) and isinstance(guard, Const))):
271
272
  return guard
272
273
  val = settings.resolve(guard, node, parents)
273
274
  if not guard.is_pattern():
@@ -276,13 +277,13 @@ def guard_matcher(guard, settings=base.SETTINGS, node=None, parents=()):
276
277
  return containers.ValueGroup(*(Const(v) for v in val))
277
278
 
278
279
 
279
- def guard_passes(pred_op, val, guard, settings=base.SETTINGS, node=None, parents=()):
280
+ def guard_passes(pred_op, val, guard, settings=base.SETTINGS, node=None, parents=(), position='guards'):
280
281
  """
281
282
  True if val satisfies pred_op against guard (see guard_matcher); a
282
283
  guard reference that does not resolve passes nothing.
283
284
  """
284
285
  try:
285
- matcher = guard_matcher(guard, settings, node, parents)
286
+ matcher = guard_matcher(guard, settings, node, parents, position)
286
287
  except base.UnresolvedReference:
287
288
  return False
288
289
  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.0
3
+ Version: 0.49.2
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
@@ -1396,7 +1396,8 @@ To chain through the items, use a pattern instead:
1396
1396
  ## Substitutions and References
1397
1397
 
1398
1398
  All `$`-prefixed syntax falls into two categories: **substitutions** (resolved
1399
- at replace time) and **references** (resolved during traversal).
1399
+ at replace time) and **references**, inline expressions evaluated during
1400
+ traversal.
1400
1401
 
1401
1402
  | Syntax | Type | Resolved against |
1402
1403
  |---|---|---|
@@ -1502,14 +1503,22 @@ the parsed result, so a path used many times can be compiled once:
1502
1503
  <a id="references"></a>
1503
1504
  ### References
1504
1505
 
1505
- A **reference** resolves a dotted path against the root object during traversal
1506
- and uses the result as a literal key. The syntax is `$$(path)`:
1506
+ A **reference**, `$$(expr)`, is an inline evaluation expression. The dotted
1507
+ expression inside is evaluated during traversal, as `get` would evaluate it
1508
+ against the root, and its value stands where the reference is written. The
1509
+ expression is a full dotted path, so it may carry patterns and transforms:
1510
+ `$$(config.offset|mul:10)` is the value of `config.offset`, times ten.
1511
+
1512
+ Where the reference stands decides what its value does. In a transform
1513
+ argument, a guard value or a filter value it is the value itself (see
1514
+ [References in transforms and guards](#references-in-transforms-and-guards)).
1515
+ In an access position, as a key or slot, it is the key to look up:
1507
1516
 
1508
1517
  >>> data = {'config': {'field': 'name'}, 'name': 'Alice'}
1509
1518
  >>> dotted.get(data, '$$(config.field)')
1510
1519
  'Alice'
1511
1520
 
1512
- Here `$$(config.field)` resolves to `'name'`, then looks up `data['name']`.
1521
+ Here `$$(config.field)` evaluates to `'name'`, then looks up `data['name']`.
1513
1522
 
1514
1523
  References work with all access types and in any position:
1515
1524
 
@@ -1606,6 +1615,12 @@ and so on:
1606
1615
  >>> dotted.get(data, 'n|add:$$(config.offset)')
1607
1616
  15
1608
1617
 
1618
+ The expression inside may chain transforms of its own; they apply to its value
1619
+ before it is passed:
1620
+
1621
+ >>> dotted.get(data, 'n|add:$$(config.offset|mul:10)')
1622
+ 105
1623
+
1609
1624
  >>> data = {'items': [{'price': 100, 'rate': 1.5}, {'price': 20, 'rate': 2}]}
1610
1625
  >>> dotted.get(data, 'items[*].price|mul:$$(^^rate)')
1611
1626
  (150.0, 40)
@@ -1662,14 +1677,16 @@ claim some references and leave the rest:
1662
1677
  >>> dotted.get({'amount': 10}, 'amount|sub:$$(lots.taken)', resolver=Store(store))
1663
1678
  7
1664
1679
 
1665
- A resolver that sets `literals = True` is also offered every plain token in
1666
- those positions, so a host sigil such as `$sel` can be a variable. Return
1680
+ A resolver with `literals = True` is also offered every plain token in
1681
+ those positions, so a host sigil such as `$sel` can be a variable. Set it on
1682
+ the class, or per instance with `literals=` on the constructor. Return
1667
1683
  `ref.value` to leave a token the literal it is; a key the resolver answers
1668
1684
  is not offered again:
1669
1685
 
1670
1686
  >>> class Sigils(dotted.ReferenceResolver):
1671
1687
  ... literals = True
1672
- ... def __init__(self, names):
1688
+ ... def __init__(self, names, literals=None):
1689
+ ... super().__init__(literals)
1673
1690
  ... self.names = names
1674
1691
  ... def resolve(self, ref, settings, node=None, parents=()):
1675
1692
  ... if ref.is_reference():
@@ -1681,6 +1698,18 @@ is not offered again:
1681
1698
  >>> dotted.get(users, 'users[*&name=$sel].name', resolver=Sigils({'$sel': 'bob'}))
1682
1699
  ('bob',)
1683
1700
 
1701
+ `literals` may instead name the positions it wants, as a list or tuple of
1702
+ `'segments'` (key, slot and attr segments), `'transforms'` (transform
1703
+ arguments), `'guards'` (guard values) and `'filters'` (filter values). A host
1704
+ whose variables only ever stand as arguments leaves `'segments'` out, so the
1705
+ keys of a path cost no call:
1706
+
1707
+ >>> r = Sigils({'$inc': 3}, literals=('transforms', 'guards', 'filters'))
1708
+ >>> dotted.get({'n': 5}, 'n|add:$inc', resolver=r)
1709
+ 8
1710
+
1711
+ An unknown name is a `ValueError`, on the class or the constructor.
1712
+
1684
1713
  A host variable may span several segments, `$amount.value`, which dotted has
1685
1714
  parsed as two. The path-level hook `fold(segments)` is called once with a
1686
1715
  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.0"
9
+ version = "0.49.2"
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"
@@ -11,6 +11,7 @@ same files run as Python.
11
11
  import os
12
12
 
13
13
  from setuptools import Extension, setup
14
+ from setuptools.command.build_ext import build_ext as _build_ext
14
15
 
15
16
  # The engine and the ops it runs. grammar.py, the parser, is left out on
16
17
  # purpose: pyparsing inspects its parse actions as Python functions.
@@ -47,7 +48,20 @@ def extensions():
47
48
  return cythonize(
48
49
  [Extension('dotted.' + name, ['dotted/' + name + '.py'], optional=True) for name in MODULES],
49
50
  compiler_directives={'language_level': 3},
51
+ nthreads=os.cpu_count() or 1,
50
52
  )
51
53
 
52
54
 
53
- setup(ext_modules=extensions())
55
+ class build_ext(_build_ext):
56
+ """
57
+ Compile the extensions one per core. setuptools builds them one after
58
+ another unless -j is given; the modules are independent, so a release
59
+ wheel compiles in a fraction of the time on a multi-core runner.
60
+ """
61
+ def finalize_options(self):
62
+ super().finalize_options()
63
+ if self.parallel is None:
64
+ self.parallel = os.cpu_count() or 1
65
+
66
+
67
+ setup(ext_modules=extensions(), cmdclass={'build_ext': build_ext})