dotted-notation 0.49.4__tar.gz → 0.51.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.
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/CHANGELOG.md +30 -0
- {dotted_notation-0.49.4/dotted_notation.egg-info → dotted_notation-0.51.0}/PKG-INFO +48 -1
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/README.md +47 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/access.py +21 -7
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/api.py +36 -19
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/base.py +20 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/grammar.py +146 -10
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/groups.py +13 -4
- {dotted_notation-0.49.4 → dotted_notation-0.51.0/dotted_notation.egg-info}/PKG-INFO +48 -1
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/pyproject.toml +1 -1
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/LICENSE +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/MANIFEST.in +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/__init__.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/__main__.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/access.pxd +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/base.pxd +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/cli/main.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/containers.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/engine.pxd +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/engine.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/filters.pxd +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/filters.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/groups.pxd +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/matchers.pxd +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/matchers.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/native.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/predicates.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/recursive.pxd +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/recursive.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/results.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/sql/core.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/sql/pg.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/transforms.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/utils.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/utypes.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/wrappers.pxd +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted/wrappers.py +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/setup.cfg +0 -0
- {dotted_notation-0.49.4 → dotted_notation-0.51.0}/setup.py +0 -0
|
@@ -3,6 +3,36 @@
|
|
|
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.51.0]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- `ReferenceResolver.lex(text, position)`, a parse-time hook for the
|
|
10
|
+
tokens dotted's grammar cannot read: a slot, `merged[k]`, a slice
|
|
11
|
+
bound, `items[:n]`, a filter value, `[*&cur=lot.cur]`, or a guard
|
|
12
|
+
value, `a=k`. A dotted name is one token. It answers with dotted text
|
|
13
|
+
read in the token's place (`'$$(k)'`), `None` to leave the
|
|
14
|
+
`ParseError`, or raises to fail the parse at the token. Tokens dotted
|
|
15
|
+
reads are never offered; `literals` remaps those at run time. `parse`
|
|
16
|
+
takes `resolver=`, and the calls taking one parse with it (#66).
|
|
17
|
+
|
|
18
|
+
## [0.50.0]
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **Breaking**: `[+]` and `[+?]` are the slot past the end, so nothing
|
|
22
|
+
is ever there to read. `get` through them is the default, not the
|
|
23
|
+
last item (use `[-1]`); `has` is `False`; `setdefault` always appends,
|
|
24
|
+
`[+?]` only when the value is absent, and returns the value it set;
|
|
25
|
+
`remove`, `pluck` and `expand` through them find nothing; a guard on
|
|
26
|
+
them, `[+]=2`, never passes.
|
|
27
|
+
- In an update, a group branch holding `[+]` always applies, since a
|
|
28
|
+
write to it always does. `[(*&k=1, +)]` updates the match and appends,
|
|
29
|
+
as before, and `(a, b[+])` now appends to `b` when it is empty too;
|
|
30
|
+
with a cut, `[(*&k=1#, +)]`, the append is still the fallback.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- `update` through a softcut group with an append branch,
|
|
34
|
+
`[(*&k=1##, +)]`, raised `AttributeError` once the filter matched.
|
|
35
|
+
|
|
6
36
|
## [0.49.4]
|
|
7
37
|
|
|
8
38
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.51.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
|
|
@@ -275,6 +275,18 @@ Several Python libraries handle nested data access. Here's how dotted compares:
|
|
|
275
275
|
<a id="breaking-changes"></a>
|
|
276
276
|
## Breaking Changes
|
|
277
277
|
|
|
278
|
+
### v0.50.0
|
|
279
|
+
- **Nothing is read through `[+]` or `[+?]`.** They are the slot past the
|
|
280
|
+
end, so `get(d, 'a[+]')` is the default, not the last item; use `a[-1]`.
|
|
281
|
+
`has` is `False`, `remove`, `pluck` and `expand` through them find
|
|
282
|
+
nothing, a guard on them never passes, and `setdefault` always appends
|
|
283
|
+
(`[+?]` only when the value is absent). See
|
|
284
|
+
[The append `+` operator](#the-append--operator).
|
|
285
|
+
- **An append branch in a group always applies in an update.**
|
|
286
|
+
`(a, b[+])` appends to `b` whether or not it is empty; it used to append
|
|
287
|
+
only when `b` already had items. With a cut, `[(*&k=1#, +)]`, the append
|
|
288
|
+
is still the fallback.
|
|
289
|
+
|
|
278
290
|
### v0.49.0
|
|
279
291
|
- **`;` is now a reserved character**: it packs a group, `(a,b;)` (see
|
|
280
292
|
[Packing](#packing)). If you have keys containing a literal `;`, quote
|
|
@@ -1757,6 +1769,29 @@ A path handed in already parsed goes through `fold()` again, so a hook must
|
|
|
1757
1769
|
be content to see its own output. `Dotted.segments()` gives any parsed
|
|
1758
1770
|
path's segment values, and a slice of a parsed path, `ops[1:]`, is a path.
|
|
1759
1771
|
|
|
1772
|
+
A host's own names may stand where dotted has no reading for a bare token:
|
|
1773
|
+
a slot, `merged[k]`, a slice bound, `items[:n]`, a filter value,
|
|
1774
|
+
`[*&cur=want]`, or a guard value, `a=k`. The parse-time hook
|
|
1775
|
+
`lex(text, position)` is offered each such token as written, a dotted name
|
|
1776
|
+
whole, with its position, one of `'slots'`, `'slices'`, `'filters'` or
|
|
1777
|
+
`'guards'`. It answers with dotted text to read in the token's place, `None`
|
|
1778
|
+
to leave the `ParseError`, or raises to fail the parse at the token. The
|
|
1779
|
+
answer must be one token dotted reads there, and is not offered again.
|
|
1780
|
+
|
|
1781
|
+
>>> class Names(dotted.ReferenceResolver):
|
|
1782
|
+
... def lex(self, text, position):
|
|
1783
|
+
... return f'$$({text})'
|
|
1784
|
+
>>> d = {'merged': {'x': 1, 'y': 2}, 'k': 'x', 'c': {'account': 'y'}}
|
|
1785
|
+
>>> dotted.get(d, 'merged[k]', resolver=Names())
|
|
1786
|
+
1
|
|
1787
|
+
>>> dotted.get(d, 'merged[c.account]', resolver=Names())
|
|
1788
|
+
2
|
|
1789
|
+
|
|
1790
|
+
A token dotted reads, `m[$x]` or `|sub:taken`, is never offered to `lex()`;
|
|
1791
|
+
a host that means something else by one remaps it at run time, through
|
|
1792
|
+
`literals`. `parse` takes `resolver=` too, and a path is cached per resolver
|
|
1793
|
+
that lexes.
|
|
1794
|
+
|
|
1760
1795
|
<a id="escaping"></a>
|
|
1761
1796
|
### Escaping
|
|
1762
1797
|
|
|
@@ -2403,6 +2438,18 @@ sequence. You may append an item or slice to the end a sequence.
|
|
|
2403
2438
|
>>> dotted.update(d, '*.there[+:]', [999])
|
|
2404
2439
|
{'hi': {'there': [1, 2, 3, 8, 999]}, 'bye': {'there': [4, 5, 6, 8, 999]}}
|
|
2405
2440
|
|
|
2441
|
+
`[+]` is the slot past the end, so nothing is ever there to read: a `get`
|
|
2442
|
+
through it is the default, and `[-1]` is the last item. A write to it always
|
|
2443
|
+
applies, so in a group an append branch is updated along with any branch
|
|
2444
|
+
that matches:
|
|
2445
|
+
|
|
2446
|
+
>>> dotted.get(d, 'hi.there[+]') is None
|
|
2447
|
+
True
|
|
2448
|
+
>>> dotted.update([{'k': 1}, {'k': 2}], '[(*&k=1, +)]', {'k': 9})
|
|
2449
|
+
[{'k': 9}, {'k': 2}, {'k': 9}]
|
|
2450
|
+
>>> dotted.update([{'k': 1}, {'k': 2}], '[(*&k=1#, +)]', {'k': 9})
|
|
2451
|
+
[{'k': 9}, {'k': 2}]
|
|
2452
|
+
|
|
2406
2453
|
<a id="the-append-unique--operator"></a>
|
|
2407
2454
|
### The append-unique `+?` operator
|
|
2408
2455
|
|
|
@@ -227,6 +227,18 @@ Several Python libraries handle nested data access. Here's how dotted compares:
|
|
|
227
227
|
<a id="breaking-changes"></a>
|
|
228
228
|
## Breaking Changes
|
|
229
229
|
|
|
230
|
+
### v0.50.0
|
|
231
|
+
- **Nothing is read through `[+]` or `[+?]`.** They are the slot past the
|
|
232
|
+
end, so `get(d, 'a[+]')` is the default, not the last item; use `a[-1]`.
|
|
233
|
+
`has` is `False`, `remove`, `pluck` and `expand` through them find
|
|
234
|
+
nothing, a guard on them never passes, and `setdefault` always appends
|
|
235
|
+
(`[+?]` only when the value is absent). See
|
|
236
|
+
[The append `+` operator](#the-append--operator).
|
|
237
|
+
- **An append branch in a group always applies in an update.**
|
|
238
|
+
`(a, b[+])` appends to `b` whether or not it is empty; it used to append
|
|
239
|
+
only when `b` already had items. With a cut, `[(*&k=1#, +)]`, the append
|
|
240
|
+
is still the fallback.
|
|
241
|
+
|
|
230
242
|
### v0.49.0
|
|
231
243
|
- **`;` is now a reserved character**: it packs a group, `(a,b;)` (see
|
|
232
244
|
[Packing](#packing)). If you have keys containing a literal `;`, quote
|
|
@@ -1709,6 +1721,29 @@ A path handed in already parsed goes through `fold()` again, so a hook must
|
|
|
1709
1721
|
be content to see its own output. `Dotted.segments()` gives any parsed
|
|
1710
1722
|
path's segment values, and a slice of a parsed path, `ops[1:]`, is a path.
|
|
1711
1723
|
|
|
1724
|
+
A host's own names may stand where dotted has no reading for a bare token:
|
|
1725
|
+
a slot, `merged[k]`, a slice bound, `items[:n]`, a filter value,
|
|
1726
|
+
`[*&cur=want]`, or a guard value, `a=k`. The parse-time hook
|
|
1727
|
+
`lex(text, position)` is offered each such token as written, a dotted name
|
|
1728
|
+
whole, with its position, one of `'slots'`, `'slices'`, `'filters'` or
|
|
1729
|
+
`'guards'`. It answers with dotted text to read in the token's place, `None`
|
|
1730
|
+
to leave the `ParseError`, or raises to fail the parse at the token. The
|
|
1731
|
+
answer must be one token dotted reads there, and is not offered again.
|
|
1732
|
+
|
|
1733
|
+
>>> class Names(dotted.ReferenceResolver):
|
|
1734
|
+
... def lex(self, text, position):
|
|
1735
|
+
... return f'$$({text})'
|
|
1736
|
+
>>> d = {'merged': {'x': 1, 'y': 2}, 'k': 'x', 'c': {'account': 'y'}}
|
|
1737
|
+
>>> dotted.get(d, 'merged[k]', resolver=Names())
|
|
1738
|
+
1
|
|
1739
|
+
>>> dotted.get(d, 'merged[c.account]', resolver=Names())
|
|
1740
|
+
2
|
|
1741
|
+
|
|
1742
|
+
A token dotted reads, `m[$x]` or `|sub:taken`, is never offered to `lex()`;
|
|
1743
|
+
a host that means something else by one remaps it at run time, through
|
|
1744
|
+
`literals`. `parse` takes `resolver=` too, and a path is cached per resolver
|
|
1745
|
+
that lexes.
|
|
1746
|
+
|
|
1712
1747
|
<a id="escaping"></a>
|
|
1713
1748
|
### Escaping
|
|
1714
1749
|
|
|
@@ -2355,6 +2390,18 @@ sequence. You may append an item or slice to the end a sequence.
|
|
|
2355
2390
|
>>> dotted.update(d, '*.there[+:]', [999])
|
|
2356
2391
|
{'hi': {'there': [1, 2, 3, 8, 999]}, 'bye': {'there': [4, 5, 6, 8, 999]}}
|
|
2357
2392
|
|
|
2393
|
+
`[+]` is the slot past the end, so nothing is ever there to read: a `get`
|
|
2394
|
+
through it is the default, and `[-1]` is the last item. A write to it always
|
|
2395
|
+
applies, so in a group an append branch is updated along with any branch
|
|
2396
|
+
that matches:
|
|
2397
|
+
|
|
2398
|
+
>>> dotted.get(d, 'hi.there[+]') is None
|
|
2399
|
+
True
|
|
2400
|
+
>>> dotted.update([{'k': 1}, {'k': 2}], '[(*&k=1, +)]', {'k': 9})
|
|
2401
|
+
[{'k': 9}, {'k': 2}, {'k': 9}]
|
|
2402
|
+
>>> dotted.update([{'k': 1}, {'k': 2}], '[(*&k=1#, +)]', {'k': 9})
|
|
2403
|
+
[{'k': 9}, {'k': 2}]
|
|
2404
|
+
|
|
2358
2405
|
<a id="the-append-unique--operator"></a>
|
|
2359
2406
|
### The append-unique `+?` operator
|
|
2360
2407
|
|
|
@@ -816,17 +816,31 @@ class SlotSpecial(Slot):
|
|
|
816
816
|
return []
|
|
817
817
|
|
|
818
818
|
def items(self, node, settings=base.SETTINGS, filtered=True):
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
except (TypeError, IndexError):
|
|
825
|
-
pass
|
|
819
|
+
"""
|
|
820
|
+
Nothing: an append slot is the one past the end, so there is never
|
|
821
|
+
a value there to read. A write to it always applies (see update).
|
|
822
|
+
"""
|
|
823
|
+
return iter(())
|
|
826
824
|
|
|
827
825
|
def is_empty(self, node):
|
|
828
826
|
return True
|
|
829
827
|
|
|
828
|
+
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, settings=base.SETTINGS):
|
|
829
|
+
"""
|
|
830
|
+
In a default skeleton (has_defaults, see engine.build_default),
|
|
831
|
+
upsert already appended this slot's placeholder, so the rest of the
|
|
832
|
+
path is written into that last item instead of a new one.
|
|
833
|
+
"""
|
|
834
|
+
if not (ops and has_defaults):
|
|
835
|
+
return super().do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap, settings=settings)
|
|
836
|
+
try:
|
|
837
|
+
last = node[-1]
|
|
838
|
+
except (TypeError, IndexError):
|
|
839
|
+
return node
|
|
840
|
+
pass_nop = nop and not nop_from_unwrap
|
|
841
|
+
built = engine.updates(ops, last, val, True, _path + [(self, -1)], pass_nop, settings=settings.below(node))
|
|
842
|
+
return self.update(node, -1, built)
|
|
843
|
+
|
|
830
844
|
def update(self, node, key, val):
|
|
831
845
|
if not isinstance(key, str):
|
|
832
846
|
return super().update(node, key, val)
|
|
@@ -8,6 +8,7 @@ import threading
|
|
|
8
8
|
import pyparsing as pp
|
|
9
9
|
from . import grammar
|
|
10
10
|
from . import engine
|
|
11
|
+
from . import groups
|
|
11
12
|
from . import results
|
|
12
13
|
from . import transforms
|
|
13
14
|
from . import base
|
|
@@ -98,21 +99,35 @@ def set_parse_cache(size=CACHE_SIZE):
|
|
|
98
99
|
|
|
99
100
|
|
|
100
101
|
@functools.lru_cache(CACHE_SIZE)
|
|
101
|
-
def _parse(ops):
|
|
102
|
-
with _parse_lock:
|
|
102
|
+
def _parse(ops, lexer=None):
|
|
103
|
+
with _parse_lock, grammar.lexing(lexer):
|
|
103
104
|
try:
|
|
104
105
|
parsed = grammar.template.parse_string(ops, parse_all=True)
|
|
105
|
-
except pp.
|
|
106
|
+
except pp.ParseBaseException as e:
|
|
106
107
|
raise ParseError(f"Invalid dotted notation: {e.msg}\n {repr(ops)}\n {' ' * e.loc}^") from None
|
|
107
108
|
return results.Dotted({'ops': parsed['ops']})
|
|
108
109
|
|
|
109
110
|
|
|
110
|
-
def
|
|
111
|
+
def _lexer(resolver):
|
|
112
|
+
"""
|
|
113
|
+
The resolver whose lex() a parse offers what dotted cannot read: the
|
|
114
|
+
call's own or the process default, or None when it keeps the default,
|
|
115
|
+
which lexes nothing, so the parse is cached by path alone.
|
|
116
|
+
"""
|
|
117
|
+
r = base.RESOLVER if resolver is None else resolver
|
|
118
|
+
if type(r).lex is base.Resolver.lex:
|
|
119
|
+
return None
|
|
120
|
+
return r
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def parse(path, bindings=None, partial=True, resolver=None):
|
|
111
124
|
"""
|
|
112
125
|
Parse dotted notation. Results are LRU-cached (same path string reuses cached parse).
|
|
113
126
|
|
|
114
127
|
If bindings is provided, resolve substitutions against them.
|
|
115
128
|
If partial is False, raise TypeError on unresolved templates.
|
|
129
|
+
A token dotted cannot read is offered to the resolver's lex(), the
|
|
130
|
+
call's own or the process default (see ReferenceResolver).
|
|
116
131
|
|
|
117
132
|
>>> parse('hello.there|str:"=%s"')
|
|
118
133
|
Dotted([hello, there|str:'=%s'])
|
|
@@ -122,7 +137,7 @@ def parse(path, bindings=None, partial=True):
|
|
|
122
137
|
elif isinstance(path, tuple):
|
|
123
138
|
ops = results.Dotted({'ops': path, 'transforms': ()})
|
|
124
139
|
else:
|
|
125
|
-
ops = _parse(path)
|
|
140
|
+
ops = _parse(path, _lexer(resolver))
|
|
126
141
|
if bindings is not None:
|
|
127
142
|
ops = ops.resolve(bindings, partial=partial)
|
|
128
143
|
if not partial and _is_template(ops):
|
|
@@ -366,7 +381,7 @@ def mutable(obj, path, strict=False, bindings=None, resolver=None):
|
|
|
366
381
|
>>> mutable(((1, 2),), '[0][0]')
|
|
367
382
|
False
|
|
368
383
|
"""
|
|
369
|
-
ops = parse(path, bindings=bindings, partial=False)
|
|
384
|
+
ops = parse(path, bindings=bindings, partial=False, resolver=resolver)
|
|
370
385
|
|
|
371
386
|
# Empty path can never mutate (can't rebind caller's variable)
|
|
372
387
|
if len(ops) == 1 and isinstance(ops[0], access.Empty):
|
|
@@ -427,7 +442,7 @@ def build_multi(obj, paths, strict=False, bindings=None, resolver=None):
|
|
|
427
442
|
if obj is AUTO:
|
|
428
443
|
obj = _auto_root(((p, None) for p in paths))
|
|
429
444
|
for path in paths:
|
|
430
|
-
ops = _prepare(parse(path, bindings=bindings, partial=False), resolver)
|
|
445
|
+
ops = _prepare(parse(path, bindings=bindings, partial=False, resolver=resolver), resolver)
|
|
431
446
|
built = engine.build(ops, obj, strict=strict, resolver=resolver)
|
|
432
447
|
# walked paths are concrete: nothing for update() to resolve or check
|
|
433
448
|
for found, val in _pluck_parsed(built, ops, strict=strict, resolver=resolver):
|
|
@@ -496,7 +511,7 @@ def get(obj, path, default=None, pattern_default=(), apply_transforms=True, stri
|
|
|
496
511
|
>>> get({'a': {'b': 7, 'c': 3}}, '**=7')
|
|
497
512
|
(7,)
|
|
498
513
|
"""
|
|
499
|
-
ops = _prepare(parse(path, bindings=bindings, partial=False), resolver)
|
|
514
|
+
ops = _prepare(parse(path, bindings=bindings, partial=False, resolver=resolver), resolver)
|
|
500
515
|
chain = ops.simple_chain if _SIMPLE_FASTPATH and not _literals(resolver) else None
|
|
501
516
|
if chain is not None:
|
|
502
517
|
val = engine.simple_get(chain, obj, strict=strict)
|
|
@@ -550,13 +565,15 @@ def setdefault(obj, path, val, apply_transforms=True, strict=False, bindings=Non
|
|
|
550
565
|
>>> setdefault({}, 'a.b.c', 7)
|
|
551
566
|
7
|
|
552
567
|
"""
|
|
553
|
-
ops = _prepare(parse(path, bindings=bindings, partial=False), resolver)
|
|
568
|
+
ops = _prepare(parse(path, bindings=bindings, partial=False, resolver=resolver), resolver)
|
|
554
569
|
if obj is AUTO:
|
|
555
570
|
obj = _auto_root_from_path(ops)
|
|
556
571
|
if has(obj, ops, strict=strict, resolver=resolver):
|
|
557
572
|
return get(obj, ops, apply_transforms=apply_transforms, strict=strict, resolver=resolver)
|
|
558
573
|
obj = update(obj, ops, val, apply_transforms=apply_transforms, strict=strict, resolver=resolver)
|
|
559
|
-
|
|
574
|
+
# an append slot, [+], has nothing to read back: what was set is val
|
|
575
|
+
default = val if groups.appends(ops) else None
|
|
576
|
+
return get(obj, ops, default=default, apply_transforms=False, strict=strict, resolver=resolver)
|
|
560
577
|
|
|
561
578
|
|
|
562
579
|
def setdefault_multi(obj, pathvalues, apply_transforms=True, strict=False, bindings=None, resolver=None):
|
|
@@ -590,7 +607,7 @@ def update_if(obj, path, val, pred=lambda val: val is not None, mutable=True, ap
|
|
|
590
607
|
"""
|
|
591
608
|
parsed = path
|
|
592
609
|
if obj is AUTO:
|
|
593
|
-
parsed = parse(path)
|
|
610
|
+
parsed = parse(path, resolver=resolver)
|
|
594
611
|
obj = _auto_root_from_path(parsed)
|
|
595
612
|
if not mutable and _is_mutable_container(obj):
|
|
596
613
|
obj = utils.deepcopy(obj)
|
|
@@ -599,7 +616,7 @@ def update_if(obj, path, val, pred=lambda val: val is not None, mutable=True, ap
|
|
|
599
616
|
if pred is not None and not pred(val):
|
|
600
617
|
return obj
|
|
601
618
|
|
|
602
|
-
ops = _prepare(parse(parsed, bindings=bindings, partial=False), resolver)
|
|
619
|
+
ops = _prepare(parse(parsed, bindings=bindings, partial=False, resolver=resolver), resolver)
|
|
603
620
|
return engine.updates(ops, obj, val, strict=strict, resolver=resolver, transforms=apply_transforms)
|
|
604
621
|
|
|
605
622
|
|
|
@@ -694,7 +711,7 @@ def remove_if(obj, path, pred=lambda path: path is not None, val=ANY, mutable=Tr
|
|
|
694
711
|
"""
|
|
695
712
|
parsed = path
|
|
696
713
|
if obj is AUTO:
|
|
697
|
-
parsed = parse(path)
|
|
714
|
+
parsed = parse(path, resolver=resolver)
|
|
698
715
|
obj = _auto_root_from_path(parsed)
|
|
699
716
|
if not mutable and _is_mutable_container(obj):
|
|
700
717
|
obj = utils.deepcopy(obj)
|
|
@@ -703,7 +720,7 @@ def remove_if(obj, path, pred=lambda path: path is not None, val=ANY, mutable=Tr
|
|
|
703
720
|
if pred is not None and not pred(path):
|
|
704
721
|
return obj
|
|
705
722
|
|
|
706
|
-
ops = _prepare(parse(parsed, bindings=bindings, partial=False), resolver)
|
|
723
|
+
ops = _prepare(parse(parsed, bindings=bindings, partial=False, resolver=resolver), resolver)
|
|
707
724
|
return engine.removes(ops, obj, val, strict=strict, resolver=resolver)
|
|
708
725
|
|
|
709
726
|
|
|
@@ -1071,7 +1088,7 @@ def expand_multi(obj, patterns, strict=False, bindings=None, resolver=None):
|
|
|
1071
1088
|
"""
|
|
1072
1089
|
seen = {}
|
|
1073
1090
|
for pat in patterns:
|
|
1074
|
-
ops = _prepare(parse(pat, bindings=bindings, partial=False), resolver)
|
|
1091
|
+
ops = _prepare(parse(pat, bindings=bindings, partial=False, resolver=resolver), resolver)
|
|
1075
1092
|
paths = (o.assemble() for o in engine.expands(ops, obj, strict=strict, resolver=resolver))
|
|
1076
1093
|
for found in paths:
|
|
1077
1094
|
if found in seen:
|
|
@@ -1106,7 +1123,7 @@ def apply_multi(obj, patterns, strict=False, bindings=None, resolver=None):
|
|
|
1106
1123
|
seen = {}
|
|
1107
1124
|
_marker = object()
|
|
1108
1125
|
for pat in patterns:
|
|
1109
|
-
for ops in engine.expands(_prepare(parse(pat, bindings=bindings, partial=False), resolver), obj, strict=strict, resolver=resolver):
|
|
1126
|
+
for ops in engine.expands(_prepare(parse(pat, bindings=bindings, partial=False, resolver=resolver), resolver), obj, strict=strict, resolver=resolver):
|
|
1110
1127
|
if ops in seen:
|
|
1111
1128
|
continue
|
|
1112
1129
|
seen[ops] = None
|
|
@@ -1147,7 +1164,7 @@ def _pluck_written(obj, patterns, strict=False, bindings=None, resolver=None):
|
|
|
1147
1164
|
"""
|
|
1148
1165
|
seen = {}
|
|
1149
1166
|
for pattern in patterns:
|
|
1150
|
-
ops = _prepare(parse(pattern, bindings=bindings, partial=False), resolver)
|
|
1167
|
+
ops = _prepare(parse(pattern, bindings=bindings, partial=False, resolver=resolver), resolver)
|
|
1151
1168
|
for path, val in engine.walk(ops, obj, paths=True, strict=strict, resolver=resolver, transforms=False):
|
|
1152
1169
|
if path is utypes.CUT_SENTINEL:
|
|
1153
1170
|
break
|
|
@@ -1167,7 +1184,7 @@ def pluck(obj, pattern, default=None, strict=False, bindings=None, resolver=None
|
|
|
1167
1184
|
>>> pluck(d, 'a.b')
|
|
1168
1185
|
('a.b', 'seven')
|
|
1169
1186
|
"""
|
|
1170
|
-
parsed = parse(pattern)
|
|
1187
|
+
parsed = parse(pattern, resolver=resolver)
|
|
1171
1188
|
out = tuple(pluck_multi(obj, (parsed,), default=default, strict=strict, resolver=resolver, bindings=bindings))
|
|
1172
1189
|
if not out:
|
|
1173
1190
|
return ()
|
|
@@ -1185,7 +1202,7 @@ def walk(obj, pattern, strict=False, bindings=None, resolver=None):
|
|
|
1185
1202
|
>>> list(walk(d, 'a.*'))
|
|
1186
1203
|
[('a.b', 1), ('a.c', 2)]
|
|
1187
1204
|
"""
|
|
1188
|
-
ops = _prepare(parse(pattern, bindings=bindings, partial=False), resolver)
|
|
1205
|
+
ops = _prepare(parse(pattern, bindings=bindings, partial=False, resolver=resolver), resolver)
|
|
1189
1206
|
for path, val in engine.walk(ops, obj, paths=True, strict=strict, resolver=resolver, transforms=False):
|
|
1190
1207
|
if path is utypes.CUT_SENTINEL:
|
|
1191
1208
|
break
|
|
@@ -154,6 +154,10 @@ class NOP(metaclass=MetaNOP):
|
|
|
154
154
|
# and filter values.
|
|
155
155
|
LITERALS = ('segments', 'transforms', 'guards', 'filters')
|
|
156
156
|
|
|
157
|
+
# The positions a resolver's lex() may be offered a token in: a slot, a
|
|
158
|
+
# slice bound, a filter value and a guard value.
|
|
159
|
+
LEXED = ('slots', 'slices', 'filters', 'guards')
|
|
160
|
+
|
|
157
161
|
|
|
158
162
|
class Resolver:
|
|
159
163
|
"""
|
|
@@ -163,6 +167,7 @@ class Resolver:
|
|
|
163
167
|
in and that node's ancestors. This default reads it as dotted does:
|
|
164
168
|
a path against the root, or with ^ against the node, ^^ its parent,
|
|
165
169
|
and so on. Raises UnresolvedReference when there is nothing there.
|
|
170
|
+
Its lex() is the parse-time hook for tokens dotted cannot read.
|
|
166
171
|
"""
|
|
167
172
|
|
|
168
173
|
# True to be offered every plain token in a var-permitting position as
|
|
@@ -191,6 +196,21 @@ class Resolver:
|
|
|
191
196
|
check_literals(literals)
|
|
192
197
|
self.literals = literals
|
|
193
198
|
|
|
199
|
+
def lex(self, text, position):
|
|
200
|
+
"""
|
|
201
|
+
The parse-time hook, offered a token dotted's grammar has no
|
|
202
|
+
reading for: text as written, merged[k] offering 'k', and the
|
|
203
|
+
position it stands in, one of LEXED. A dotted name is one token,
|
|
204
|
+
merged[a.b] offering 'a.b'. It answers with dotted text to read
|
|
205
|
+
in the token's place, '$$(k)' making the path merged[$$(k)], or
|
|
206
|
+
None to leave the path unparsed; raising fails the parse at the
|
|
207
|
+
token. The text is read in that position as one token, and is
|
|
208
|
+
not offered to lex() again. A token dotted reads is never
|
|
209
|
+
offered: a host that means something else by one says so at run
|
|
210
|
+
time, through literals. The default lexes nothing.
|
|
211
|
+
"""
|
|
212
|
+
return None
|
|
213
|
+
|
|
194
214
|
def fold(self, segments):
|
|
195
215
|
"""
|
|
196
216
|
The path-level hook, called once with a path's segment strings
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
"""
|
|
3
|
+
import contextlib
|
|
3
4
|
import decimal
|
|
4
5
|
import re
|
|
5
6
|
import pyparsing as pp
|
|
@@ -288,10 +289,134 @@ var = _escaped_dollar | _reference | _paren_subst | _raw_subst | _dollar_word
|
|
|
288
289
|
# |min:$amount.value or =^lots.taken: one string, for a resolver to read
|
|
289
290
|
# whole. In a segment position the . separates segments, so not there.
|
|
290
291
|
_arg_sigil = pp.Regex(r'(?:\$(?![0-9($])|\^)[^' + re.escape(reserved.replace('.', '')) + r' \t\n\r]+').set_parse_action(matchers.Word)
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
# Lexed tokens: what dotted cannot read in a slot, a slice bound, a filter
|
|
295
|
+
# value or a guard value is offered to the resolver's lex() (see
|
|
296
|
+
# base.Resolver.lex), last, once dotted's own readings have failed.
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
# The resolver a path is being parsed for, when it lexes, and its answers
|
|
300
|
+
# so far, by (text, position): reading an answer resets pyparsing's
|
|
301
|
+
# packrat cache, so the parse may come back to a token it has lexed.
|
|
302
|
+
LEXER = None
|
|
303
|
+
_ANSWERS = {}
|
|
304
|
+
|
|
305
|
+
_WORD_CHARS = frozenset(pp.alphanums + '_')
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@contextlib.contextmanager
|
|
309
|
+
def lexing(lexer):
|
|
310
|
+
"""
|
|
311
|
+
Parse with lexer offered what dotted cannot read (see Lexed); None
|
|
312
|
+
for no lexer. Not reentrant: api._parse holds its lock around it.
|
|
313
|
+
"""
|
|
314
|
+
global LEXER
|
|
315
|
+
LEXER = lexer
|
|
316
|
+
_ANSWERS.clear()
|
|
317
|
+
try:
|
|
318
|
+
yield
|
|
319
|
+
finally:
|
|
320
|
+
LEXER = None
|
|
321
|
+
_ANSWERS.clear()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _token_end(s, loc, stops):
|
|
325
|
+
"""
|
|
326
|
+
Where the token at loc ends: at the first of stops, whitespace or a
|
|
327
|
+
closing bracket outside the brackets and quotes it opens itself.
|
|
328
|
+
"""
|
|
329
|
+
depth = 0
|
|
330
|
+
quote = None
|
|
331
|
+
i = loc
|
|
332
|
+
while i < len(s):
|
|
333
|
+
c = s[i]
|
|
334
|
+
if quote:
|
|
335
|
+
if c == '\\':
|
|
336
|
+
i += 2
|
|
337
|
+
continue
|
|
338
|
+
if c == quote:
|
|
339
|
+
quote = None
|
|
340
|
+
elif c in '"\'':
|
|
341
|
+
quote = c
|
|
342
|
+
elif c in '([{':
|
|
343
|
+
depth += 1
|
|
344
|
+
elif depth:
|
|
345
|
+
if c in ')]}':
|
|
346
|
+
depth -= 1
|
|
347
|
+
elif c in stops or c in ')]}' or c.isspace():
|
|
348
|
+
return i
|
|
349
|
+
i += 1
|
|
350
|
+
return i
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class Lexed(pp.Token):
|
|
354
|
+
"""
|
|
355
|
+
A token dotted has no reading for at position, offered to LEXER. It
|
|
356
|
+
runs to the first of stops (see _token_end) and is offered only when
|
|
357
|
+
one of follow, or with end the end of the path, comes after it. The
|
|
358
|
+
text the lexer answers is read whole with reading, without LEXER.
|
|
359
|
+
"""
|
|
360
|
+
def __init__(self, position, reading, stops, follow=None, end=False):
|
|
361
|
+
super().__init__()
|
|
362
|
+
self.position = position
|
|
363
|
+
self.reading = reading
|
|
364
|
+
self.stops = stops
|
|
365
|
+
self.follow = stops if follow is None else follow
|
|
366
|
+
self.end = end
|
|
367
|
+
self.mayReturnEmpty = False
|
|
368
|
+
self.mayIndexError = False
|
|
369
|
+
self.errmsg = f'Expected a token in {position}'
|
|
370
|
+
|
|
371
|
+
def _generateDefaultName(self):
|
|
372
|
+
return f'Lexed({self.position})'
|
|
373
|
+
|
|
374
|
+
def parseImpl(self, instring, loc, do_actions=True):
|
|
375
|
+
global LEXER
|
|
376
|
+
lexer = LEXER
|
|
377
|
+
if lexer is None:
|
|
378
|
+
raise pp.ParseException(instring, loc, self.errmsg, self)
|
|
379
|
+
stop = _token_end(instring, loc, self.stops)
|
|
380
|
+
if stop == loc:
|
|
381
|
+
raise pp.ParseException(instring, loc, self.errmsg, self)
|
|
382
|
+
if stop == len(instring) and not self.end:
|
|
383
|
+
raise pp.ParseException(instring, loc, self.errmsg, self)
|
|
384
|
+
if stop < len(instring) and instring[stop] not in self.follow:
|
|
385
|
+
raise pp.ParseException(instring, loc, self.errmsg, self)
|
|
386
|
+
text = instring[loc:stop]
|
|
387
|
+
key = (text, self.position)
|
|
388
|
+
if key not in _ANSWERS:
|
|
389
|
+
try:
|
|
390
|
+
_ANSWERS[key] = lexer.lex(text, self.position)
|
|
391
|
+
except Exception as e:
|
|
392
|
+
raise pp.ParseFatalException(instring, loc, f'{text!r} in {self.position}: {e}') from e
|
|
393
|
+
lexed = _ANSWERS[key]
|
|
394
|
+
if lexed is None:
|
|
395
|
+
raise pp.ParseException(instring, loc, self.errmsg, self)
|
|
396
|
+
LEXER = None
|
|
397
|
+
try:
|
|
398
|
+
tokens = self.reading.parse_string(lexed, parse_all=True)
|
|
399
|
+
except pp.ParseBaseException:
|
|
400
|
+
raise pp.ParseFatalException(
|
|
401
|
+
instring, loc, f'{text!r} lexed as {lexed!r}, not one token in {self.position}') from None
|
|
402
|
+
finally:
|
|
403
|
+
LEXER = lexer
|
|
404
|
+
return stop, list(tokens)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# After a token dotted reads, while a lexer is in force: no word character
|
|
408
|
+
# follows, so null in =nullable or 3 in [:3x] is not read as a prefix of
|
|
409
|
+
# the token, which is the lexer's to read whole.
|
|
410
|
+
_ends = pp.Empty().leave_whitespace().add_condition(
|
|
411
|
+
lambda s, loc, t: LEXER is None or s[loc:loc + 1] not in _WORD_CHARS, call_during_try=True)
|
|
412
|
+
|
|
291
413
|
# A slice bound: an integer, + (the length), or a $$(path) reference or
|
|
292
|
-
# $(name)/$N substitution standing for one.
|
|
293
|
-
|
|
294
|
-
|
|
414
|
+
# $(name)/$N substitution standing for one. A start bound is lexed only
|
|
415
|
+
# before its :, so [k] is not offered as one.
|
|
416
|
+
_slice_read = integer | plus | _reference | _paren_subst | _raw_subst
|
|
417
|
+
_slice_bound = (_slice_read + _ends) | Lexed('slices', _slice_read, ':]')
|
|
418
|
+
_slice_start = (_slice_read + _ends) | Lexed('slices', _slice_read, ':]', follow=':')
|
|
419
|
+
slice = pp.Optional(_slice_start) + ':' + pp.Optional(_slice_bound) \
|
|
295
420
|
+ pp.Optional(':') + pp.Optional(_slice_bound)
|
|
296
421
|
|
|
297
422
|
_common_pats = wildcard_first | wildcard | var | regex_first | regex
|
|
@@ -548,6 +673,11 @@ value = pp.Forward()
|
|
|
548
673
|
_value_group_inner = value + OM(_ccomma + value)
|
|
549
674
|
value_group = (S('(') + _value_group_inner + S(')')).set_parse_action(lambda t: [ct.ValueGroup(*t)])
|
|
550
675
|
value <<= value_group | container_value | string_glob | bytes_literal | string | wildcard | _arg_sigil | var | regex | numeric_quoted | none | true | false | numeric_extended | numeric_slot
|
|
676
|
+
# A filter's or guard's value, else a token for the lexer: it runs to what
|
|
677
|
+
# may follow a value, a dot not among them, so =lot.x is one token.
|
|
678
|
+
_value_stops = '&,)]|?#;'
|
|
679
|
+
filter_value = (value + _ends) | Lexed('filters', value, _value_stops, end=True)
|
|
680
|
+
guard_value = (value + _ends) | Lexed('guards', value, _value_stops, end=True)
|
|
551
681
|
# ---------------------------------------------------------------------------
|
|
552
682
|
# Concat: part+part for key construction using Python's native + operator.
|
|
553
683
|
# Each part can carry per-part transforms: part|xform+part|xform
|
|
@@ -631,7 +761,7 @@ def _filter_single_action(t):
|
|
|
631
761
|
cls = flt.FILTER_PRED_CLS[op_str]
|
|
632
762
|
return cls(rest + [val])
|
|
633
763
|
|
|
634
|
-
_filter_single_all = (filter_key + ZM(pipe + transform) + _pred_op_re +
|
|
764
|
+
_filter_single_all = (filter_key + ZM(pipe + transform) + _pred_op_re + filter_value).set_parse_action(_filter_single_action)
|
|
635
765
|
|
|
636
766
|
# Recursive filter expression with grouping
|
|
637
767
|
filter_expr = pp.Forward()
|
|
@@ -736,6 +866,12 @@ slicecmd = (lb + Opt(L('~')) + Opt(slice) + rb).set_parse_action(
|
|
|
736
866
|
lambda t: wrappers.NopWrap(access.Slice(*t[1:])) if t and t[0] == '~' else access.Slice(*t))
|
|
737
867
|
slicefilter = (lb + filters + ZM(amp + filters) + rb).set_parse_action(access.SliceFilter)
|
|
738
868
|
|
|
869
|
+
# A slot dotted cannot read, [k] or [a.b], offered to the lexer: tried
|
|
870
|
+
# after every other bracket, so [a.b=1] stays a filter.
|
|
871
|
+
_slot_read = _slot_concat | _commons | numeric_extended | numeric_slot
|
|
872
|
+
lexedcmd = (lb + Opt(L('~')) + Lexed('slots', _slot_read, ']&') + ZM(amp + filters) + rb
|
|
873
|
+
+ Opt(_seg_mid)).set_parse_action(_slotcmd_action)
|
|
874
|
+
|
|
739
875
|
# Cut markers: ## = soft cut, # = hard cut (try ## first so ## isn't parsed as # + #)
|
|
740
876
|
softcut_marker = L('##')
|
|
741
877
|
cut_marker = softcut_marker | L('#')
|
|
@@ -983,7 +1119,7 @@ _at_nop_grouped = _at_nop_grouped.set_parse_action(
|
|
|
983
1119
|
)
|
|
984
1120
|
|
|
985
1121
|
# NOP (~): match but don't update
|
|
986
|
-
_nop_inner = inner_grouped_first | inner_grouped | recursive_op | keycmd | attrcmd | slotgroup_first | slotgroup | slotcmd | slotspecial | slicefilter | slicecmd
|
|
1122
|
+
_nop_inner = inner_grouped_first | inner_grouped | recursive_op | keycmd | attrcmd | slotgroup_first | slotgroup | slotcmd | slotspecial | slicefilter | slicecmd | lexedcmd
|
|
987
1123
|
_nop_wrap = (tilde + _nop_inner).set_parse_action(lambda t: wrappers.NopWrap(t[1]))
|
|
988
1124
|
|
|
989
1125
|
# Explicit NOP (~): like _nop_wrap but inner must start with an access op (./@/[]).
|
|
@@ -998,7 +1134,7 @@ _explicit_nop_inner = (
|
|
|
998
1134
|
attrcmd |
|
|
999
1135
|
slotgroup_first | slotgroup |
|
|
1000
1136
|
slotcmd |
|
|
1001
|
-
slotspecial | slicefilter | slicecmd
|
|
1137
|
+
slotspecial | slicefilter | slicecmd | lexedcmd
|
|
1002
1138
|
)
|
|
1003
1139
|
_explicit_nop_wrap = (tilde + _explicit_nop_inner).set_parse_action(lambda t: wrappers.NopWrap(t[1]))
|
|
1004
1140
|
|
|
@@ -1015,7 +1151,7 @@ _op_seq_cont_items = (
|
|
|
1015
1151
|
attrcmd |
|
|
1016
1152
|
slotgroup_first | slotgroup |
|
|
1017
1153
|
slotcmd |
|
|
1018
|
-
slotspecial | slicefilter | slicecmd
|
|
1154
|
+
slotspecial | slicefilter | slicecmd | lexedcmd
|
|
1019
1155
|
)
|
|
1020
1156
|
_op_seq_cont << _op_seq_cont_items
|
|
1021
1157
|
_continues <<= pp.FollowedBy(_op_seq_cont)
|
|
@@ -1034,7 +1170,7 @@ _op_seq_first_items = (
|
|
|
1034
1170
|
attrcmd |
|
|
1035
1171
|
slotgroup_first | slotgroup |
|
|
1036
1172
|
slotcmd |
|
|
1037
|
-
slotspecial | slicefilter | slicecmd
|
|
1173
|
+
slotspecial | slicefilter | slicecmd | lexedcmd
|
|
1038
1174
|
)
|
|
1039
1175
|
# First item also allows bare grouped expressions — (a,b) with bare keys.
|
|
1040
1176
|
# This is valid at the start of a path (top-level inference).
|
|
@@ -1051,7 +1187,7 @@ op_seq_item << (inner_grouped_first | inner_grouped | _op_seq_first_items)
|
|
|
1051
1187
|
# guard, they transform the value.
|
|
1052
1188
|
# ---------------------------------------------------------------------------
|
|
1053
1189
|
|
|
1054
|
-
_postfix = Opt(type_restriction) + ZM(amp + filters) + ZM(pipe + transform) + Opt(_pred_op_re +
|
|
1190
|
+
_postfix = Opt(type_restriction) + ZM(amp + filters) + ZM(pipe + transform) + Opt(_pred_op_re + guard_value + ZM(pipe + transform))
|
|
1055
1191
|
|
|
1056
1192
|
|
|
1057
1193
|
def _split_postfix(tokens):
|
|
@@ -1148,7 +1284,7 @@ _explicit_op_seq_cont_items = (
|
|
|
1148
1284
|
attrcmd |
|
|
1149
1285
|
slotgroup_first | slotgroup |
|
|
1150
1286
|
slotcmd |
|
|
1151
|
-
slotspecial | slicefilter | slicecmd
|
|
1287
|
+
slotspecial | slicefilter | slicecmd | lexedcmd
|
|
1152
1288
|
)
|
|
1153
1289
|
_explicit_op_seq = pp.Group(_explicit_op_seq_cont_items + ZM(_explicit_op_seq_cont_items))
|
|
1154
1290
|
_explicit_inner_atom = _explicit_op_seq
|
|
@@ -5,7 +5,7 @@ from . import matchers
|
|
|
5
5
|
from . import wrappers
|
|
6
6
|
from . import engine
|
|
7
7
|
from . import results
|
|
8
|
-
from .access import AccessOp, Key, Attr, Slot
|
|
8
|
+
from .access import AccessOp, Key, Attr, Slot, SlotSpecial
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def leading_keys(path):
|
|
@@ -415,12 +415,12 @@ class OpGroupOr(OpGroup):
|
|
|
415
415
|
if softcut_paths and path and softcut_paths.overlaps(path):
|
|
416
416
|
continue
|
|
417
417
|
paths.append(path)
|
|
418
|
-
if not paths:
|
|
418
|
+
if not paths and not appends(item):
|
|
419
419
|
continue
|
|
420
420
|
matched_any = True
|
|
421
421
|
if marker is base.BRANCH_SOFTCUT:
|
|
422
422
|
softcut_paths.extend(p for p in paths if p)
|
|
423
|
-
if softcut_paths:
|
|
423
|
+
if softcut_paths and paths:
|
|
424
424
|
branch_nop = nop or any(isinstance(op, wrappers.NopWrap) for op in item)
|
|
425
425
|
for path in paths:
|
|
426
426
|
node = engine.updates(list(path), node, val, has_defaults, _path, branch_nop, settings=settings)
|
|
@@ -524,7 +524,7 @@ class OpGroupFirst(OpGroup):
|
|
|
524
524
|
branch_ops = list(branch) + list(ops)
|
|
525
525
|
if not branch_ops:
|
|
526
526
|
continue
|
|
527
|
-
if base.has_any(engine.gets(branch_ops, node, settings=settings)):
|
|
527
|
+
if appends(branch) or base.has_any(engine.gets(branch_ops, node, settings=settings)):
|
|
528
528
|
return engine.updates(branch_ops, node, val, has_defaults, _path, nop, settings=settings)
|
|
529
529
|
return _disjunction_fallback(self, ops, node, val, has_defaults, _path, nop, settings=settings)
|
|
530
530
|
|
|
@@ -968,6 +968,15 @@ def _attr_branch(branch):
|
|
|
968
968
|
return branch
|
|
969
969
|
|
|
970
970
|
|
|
971
|
+
def appends(ops):
|
|
972
|
+
"""
|
|
973
|
+
True if ops hold an append slot, [+] or [+?]. Nothing is ever there to
|
|
974
|
+
read, but a write always applies, so in an update a group branch that
|
|
975
|
+
holds one counts as matched.
|
|
976
|
+
"""
|
|
977
|
+
return any(isinstance(op.most_inner, SlotSpecial) for op in ops)
|
|
978
|
+
|
|
979
|
+
|
|
971
980
|
def _is_concrete_path(branch_ops):
|
|
972
981
|
"""
|
|
973
982
|
Return True if branch_ops represents a concrete path (no wildcards/patterns).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.51.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
|
|
@@ -275,6 +275,18 @@ Several Python libraries handle nested data access. Here's how dotted compares:
|
|
|
275
275
|
<a id="breaking-changes"></a>
|
|
276
276
|
## Breaking Changes
|
|
277
277
|
|
|
278
|
+
### v0.50.0
|
|
279
|
+
- **Nothing is read through `[+]` or `[+?]`.** They are the slot past the
|
|
280
|
+
end, so `get(d, 'a[+]')` is the default, not the last item; use `a[-1]`.
|
|
281
|
+
`has` is `False`, `remove`, `pluck` and `expand` through them find
|
|
282
|
+
nothing, a guard on them never passes, and `setdefault` always appends
|
|
283
|
+
(`[+?]` only when the value is absent). See
|
|
284
|
+
[The append `+` operator](#the-append--operator).
|
|
285
|
+
- **An append branch in a group always applies in an update.**
|
|
286
|
+
`(a, b[+])` appends to `b` whether or not it is empty; it used to append
|
|
287
|
+
only when `b` already had items. With a cut, `[(*&k=1#, +)]`, the append
|
|
288
|
+
is still the fallback.
|
|
289
|
+
|
|
278
290
|
### v0.49.0
|
|
279
291
|
- **`;` is now a reserved character**: it packs a group, `(a,b;)` (see
|
|
280
292
|
[Packing](#packing)). If you have keys containing a literal `;`, quote
|
|
@@ -1757,6 +1769,29 @@ A path handed in already parsed goes through `fold()` again, so a hook must
|
|
|
1757
1769
|
be content to see its own output. `Dotted.segments()` gives any parsed
|
|
1758
1770
|
path's segment values, and a slice of a parsed path, `ops[1:]`, is a path.
|
|
1759
1771
|
|
|
1772
|
+
A host's own names may stand where dotted has no reading for a bare token:
|
|
1773
|
+
a slot, `merged[k]`, a slice bound, `items[:n]`, a filter value,
|
|
1774
|
+
`[*&cur=want]`, or a guard value, `a=k`. The parse-time hook
|
|
1775
|
+
`lex(text, position)` is offered each such token as written, a dotted name
|
|
1776
|
+
whole, with its position, one of `'slots'`, `'slices'`, `'filters'` or
|
|
1777
|
+
`'guards'`. It answers with dotted text to read in the token's place, `None`
|
|
1778
|
+
to leave the `ParseError`, or raises to fail the parse at the token. The
|
|
1779
|
+
answer must be one token dotted reads there, and is not offered again.
|
|
1780
|
+
|
|
1781
|
+
>>> class Names(dotted.ReferenceResolver):
|
|
1782
|
+
... def lex(self, text, position):
|
|
1783
|
+
... return f'$$({text})'
|
|
1784
|
+
>>> d = {'merged': {'x': 1, 'y': 2}, 'k': 'x', 'c': {'account': 'y'}}
|
|
1785
|
+
>>> dotted.get(d, 'merged[k]', resolver=Names())
|
|
1786
|
+
1
|
|
1787
|
+
>>> dotted.get(d, 'merged[c.account]', resolver=Names())
|
|
1788
|
+
2
|
|
1789
|
+
|
|
1790
|
+
A token dotted reads, `m[$x]` or `|sub:taken`, is never offered to `lex()`;
|
|
1791
|
+
a host that means something else by one remaps it at run time, through
|
|
1792
|
+
`literals`. `parse` takes `resolver=` too, and a path is cached per resolver
|
|
1793
|
+
that lexes.
|
|
1794
|
+
|
|
1760
1795
|
<a id="escaping"></a>
|
|
1761
1796
|
### Escaping
|
|
1762
1797
|
|
|
@@ -2403,6 +2438,18 @@ sequence. You may append an item or slice to the end a sequence.
|
|
|
2403
2438
|
>>> dotted.update(d, '*.there[+:]', [999])
|
|
2404
2439
|
{'hi': {'there': [1, 2, 3, 8, 999]}, 'bye': {'there': [4, 5, 6, 8, 999]}}
|
|
2405
2440
|
|
|
2441
|
+
`[+]` is the slot past the end, so nothing is ever there to read: a `get`
|
|
2442
|
+
through it is the default, and `[-1]` is the last item. A write to it always
|
|
2443
|
+
applies, so in a group an append branch is updated along with any branch
|
|
2444
|
+
that matches:
|
|
2445
|
+
|
|
2446
|
+
>>> dotted.get(d, 'hi.there[+]') is None
|
|
2447
|
+
True
|
|
2448
|
+
>>> dotted.update([{'k': 1}, {'k': 2}], '[(*&k=1, +)]', {'k': 9})
|
|
2449
|
+
[{'k': 9}, {'k': 2}, {'k': 9}]
|
|
2450
|
+
>>> dotted.update([{'k': 1}, {'k': 2}], '[(*&k=1#, +)]', {'k': 9})
|
|
2451
|
+
[{'k': 9}, {'k': 2}]
|
|
2452
|
+
|
|
2406
2453
|
<a id="the-append-unique--operator"></a>
|
|
2407
2454
|
### The append-unique `+?` operator
|
|
2408
2455
|
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "dotted_notation"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.51.0"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dotted_notation-0.49.4 → dotted_notation-0.51.0}/dotted_notation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|