dotted-notation 0.48.1__tar.gz → 0.49.1__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.48.1 → dotted_notation-0.49.1}/CHANGELOG.md +38 -0
- {dotted_notation-0.48.1/dotted_notation.egg-info → dotted_notation-0.49.1}/PKG-INFO +100 -6
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/README.md +99 -5
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/filters.py +60 -15
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/grammar.py +42 -11
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/groups.py +117 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/sql/pg.py +6 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/transforms.py +40 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1/dotted_notation.egg-info}/PKG-INFO +100 -6
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/pyproject.toml +1 -1
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/setup.py +15 -1
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/LICENSE +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/MANIFEST.in +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/__init__.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/__main__.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/access.pxd +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/access.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/api.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/base.pxd +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/base.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/cli/main.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/containers.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/engine.pxd +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/engine.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/filters.pxd +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/groups.pxd +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/matchers.pxd +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/matchers.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/native.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/predicates.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/recursive.pxd +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/recursive.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/results.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/sql/core.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/utils.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/utypes.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/wrappers.pxd +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted/wrappers.py +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.48.1 → dotted_notation-0.49.1}/setup.cfg +0 -0
|
@@ -3,6 +3,44 @@
|
|
|
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.1]
|
|
7
|
+
|
|
8
|
+
### Documentation
|
|
9
|
+
- The README describes a reference, `$$(expr)`, as an inline evaluation
|
|
10
|
+
expression: the dotted expression inside, transforms included, is
|
|
11
|
+
evaluated during traversal and its value stands where it is written. In a
|
|
12
|
+
transform argument, guard or filter value that is the value itself; in an
|
|
13
|
+
access position it is the key looked up.
|
|
14
|
+
|
|
15
|
+
## [0.49.0]
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Packing: a `;` just inside either paren of a group, `(a,b;)` or
|
|
19
|
+
`(;a,b)`, makes the group yield its matches as one tuple instead of
|
|
20
|
+
one by one. The group keeps its kind, `(a&b;)` packs both or nothing,
|
|
21
|
+
`(a#,b;)` what the cut lets through, `(a.*;)` a one-branch group, and
|
|
22
|
+
nothing matched packs to `()`. The tuple is one value, so a packed
|
|
23
|
+
path is not a pattern and a transform after it applies once, which is
|
|
24
|
+
how an aggregate over a pattern is written: `(a.*;)|sum` is one
|
|
25
|
+
number, `items[*].(p[*];)|sum` one per item. Guards compare the
|
|
26
|
+
tuple, `(a.*;)|sum>2`, and the path may continue into it,
|
|
27
|
+
`(a.*;)[0]`. A packed group may lead a filter key, running from each
|
|
28
|
+
item: `items[(p[*];)|sum>2]` filters by an aggregate over the item.
|
|
29
|
+
`update` and `remove` through a pack raise `ValueError`; the SQL
|
|
30
|
+
translator raises `TranslationError`.
|
|
31
|
+
- `|sum` and `|mean` transforms, on any container value.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- **Breaking**: `;` is a reserved character. Quote a key that contains
|
|
35
|
+
one: `"a;b"`.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- On the compiled engine, groups and filters compared by identity, so
|
|
39
|
+
`parse('(a,b)') == parse('(a, b)')` was `False` there and `True` on
|
|
40
|
+
the Python engine. A compiled class that defines `__hash__` alone
|
|
41
|
+
does not inherit `__eq__`; `OpGroup` and the filter classes now
|
|
42
|
+
spell theirs out.
|
|
43
|
+
|
|
6
44
|
## [0.48.1]
|
|
7
45
|
|
|
8
46
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.49.1
|
|
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
|
|
@@ -274,6 +274,11 @@ Several Python libraries handle nested data access. Here's how dotted compares:
|
|
|
274
274
|
<a id="breaking-changes"></a>
|
|
275
275
|
## Breaking Changes
|
|
276
276
|
|
|
277
|
+
### v0.49.0
|
|
278
|
+
- **`;` is now a reserved character**: it packs a group, `(a,b;)` (see
|
|
279
|
+
[Packing](#packing)). If you have keys containing a literal `;`, quote
|
|
280
|
+
them: `"a;b"`.
|
|
281
|
+
|
|
277
282
|
### v0.47.0
|
|
278
283
|
- **`!` binds tighter than guards, filters and transforms.** `!a=5` is now
|
|
279
284
|
`(!a)=5`, the values other than `a`'s that equal 5; the old meaning,
|
|
@@ -1391,7 +1396,8 @@ To chain through the items, use a pattern instead:
|
|
|
1391
1396
|
## Substitutions and References
|
|
1392
1397
|
|
|
1393
1398
|
All `$`-prefixed syntax falls into two categories: **substitutions** (resolved
|
|
1394
|
-
at replace time) and **references
|
|
1399
|
+
at replace time) and **references**, inline expressions evaluated during
|
|
1400
|
+
traversal.
|
|
1395
1401
|
|
|
1396
1402
|
| Syntax | Type | Resolved against |
|
|
1397
1403
|
|---|---|---|
|
|
@@ -1497,14 +1503,22 @@ the parsed result, so a path used many times can be compiled once:
|
|
|
1497
1503
|
<a id="references"></a>
|
|
1498
1504
|
### References
|
|
1499
1505
|
|
|
1500
|
-
A **reference
|
|
1501
|
-
|
|
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:
|
|
1502
1516
|
|
|
1503
1517
|
>>> data = {'config': {'field': 'name'}, 'name': 'Alice'}
|
|
1504
1518
|
>>> dotted.get(data, '$$(config.field)')
|
|
1505
1519
|
'Alice'
|
|
1506
1520
|
|
|
1507
|
-
Here `$$(config.field)`
|
|
1521
|
+
Here `$$(config.field)` evaluates to `'name'`, then looks up `data['name']`.
|
|
1508
1522
|
|
|
1509
1523
|
References work with all access types and in any position:
|
|
1510
1524
|
|
|
@@ -1601,6 +1615,12 @@ and so on:
|
|
|
1601
1615
|
>>> dotted.get(data, 'n|add:$$(config.offset)')
|
|
1602
1616
|
15
|
|
1603
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
|
+
|
|
1604
1624
|
>>> data = {'items': [{'price': 100, 'rate': 1.5}, {'price': 20, 'rate': 2}]}
|
|
1605
1625
|
>>> dotted.get(data, 'items[*].price|mul:$$(^^rate)')
|
|
1606
1626
|
(150.0, 40)
|
|
@@ -2147,6 +2167,77 @@ fallback as disjunction—first concrete path (last to first):
|
|
|
2147
2167
|
>>> dotted.update({'a': {}}, 'a(.x,.y)?', 99) # nothing matches → creates last (.y)
|
|
2148
2168
|
{'a': {'y': 99}}
|
|
2149
2169
|
|
|
2170
|
+
#### Packing
|
|
2171
|
+
|
|
2172
|
+
A group yields its matches one by one. A `;` just inside either paren
|
|
2173
|
+
**packs** them: the group yields one value, the tuple of what it would
|
|
2174
|
+
have yielded. `(a,b;)` and `(;a,b)` are the same pack, and the group
|
|
2175
|
+
keeps its own kind—`(a,b;)` packs the branches that match, `(a&b;)` packs
|
|
2176
|
+
both or nothing, `(a#,b;)` packs what the cut lets through, `(a,b;)?`
|
|
2177
|
+
packs the first match—and `(a.*;)` packs a one-branch group. Nothing
|
|
2178
|
+
matched packs to `()`:
|
|
2179
|
+
|
|
2180
|
+
>>> d = {'a': {'x': 1, 'y': 2}, 'b': 3}
|
|
2181
|
+
>>> dotted.get(d, '(a.*;)')
|
|
2182
|
+
(1, 2)
|
|
2183
|
+
>>> dotted.get(d, '(b,zz;)')
|
|
2184
|
+
(3,)
|
|
2185
|
+
>>> dotted.get(d, '(b&zz;)')
|
|
2186
|
+
()
|
|
2187
|
+
>>> dotted.get(d, '(zz.*;)')
|
|
2188
|
+
()
|
|
2189
|
+
|
|
2190
|
+
The tuple is one value, so a packed path is not a pattern: `get` returns
|
|
2191
|
+
the value bare, and a transform after the pack applies to the tuple once.
|
|
2192
|
+
This is how an aggregate over a pattern is written—the pack gathers, an
|
|
2193
|
+
ordinary [transform](#transforms) reduces:
|
|
2194
|
+
|
|
2195
|
+
>>> dotted.get(d, '(a.*;)|sum')
|
|
2196
|
+
3
|
|
2197
|
+
>>> dotted.get(d, '(a.*;)|mean')
|
|
2198
|
+
1.5
|
|
2199
|
+
>>> dotted.get(d, '(a.*;)|set|len')
|
|
2200
|
+
2
|
|
2201
|
+
>>> dotted.get({'a': 1, 'b': 2}, '(a,b;)|sum') # not (a|sum,b|sum)
|
|
2202
|
+
3
|
|
2203
|
+
|
|
2204
|
+
The group runs from the node the pack sits at, so a pack under a pattern
|
|
2205
|
+
gives one tuple per match—a group-by—while a pack around the whole path
|
|
2206
|
+
gathers across every level inside it:
|
|
2207
|
+
|
|
2208
|
+
>>> d = {'items': [{'p': [1, 2]}, {'p': [3]}, {'p': []}]}
|
|
2209
|
+
>>> dotted.get(d, 'items[*].(p[*];)|sum')
|
|
2210
|
+
(3, 3, 0)
|
|
2211
|
+
>>> dotted.get(d, '(items[*].p[*];)|sum')
|
|
2212
|
+
6
|
|
2213
|
+
|
|
2214
|
+
The path may continue into the tuple, and a guard compares it, with
|
|
2215
|
+
[guard transforms](#guard-transforms) before it:
|
|
2216
|
+
|
|
2217
|
+
>>> d = {'a': {'x': 1, 'y': 2}}
|
|
2218
|
+
>>> dotted.get(d, '(a.*;)[0]')
|
|
2219
|
+
1
|
|
2220
|
+
>>> dotted.get(d, '(a.*;)|sum>2')
|
|
2221
|
+
(1, 2)
|
|
2222
|
+
|
|
2223
|
+
A packed group may also lead a [filter](#filters) key, where it runs
|
|
2224
|
+
from each item and its tuple is the value compared—filter by an
|
|
2225
|
+
aggregate over the item:
|
|
2226
|
+
|
|
2227
|
+
>>> d = {'items': [{'p': [1, 2]}, {'p': [3]}, {'p': []}]}
|
|
2228
|
+
>>> dotted.get(d, 'items[(p[*];)|sum>2]')
|
|
2229
|
+
[{'p': [1, 2]}, {'p': [3]}]
|
|
2230
|
+
>>> dotted.get(d, 'items[*&(p[*];)|len=0]')
|
|
2231
|
+
({'p': []},)
|
|
2232
|
+
|
|
2233
|
+
A pack is its own path segment: `walk` and `expand` give it back as
|
|
2234
|
+
written, `(a.*;)|sum`. There is nothing under a tuple of matches to write
|
|
2235
|
+
to, so `update` and `remove` through a pack raise `ValueError`, and a pack
|
|
2236
|
+
cannot be negated. `;` is a reserved character; quote a key that contains
|
|
2237
|
+
one, `"a;b"`.
|
|
2238
|
+
|
|
2239
|
+
`(a.*;)[*]` yields the matches again: `[*]` is the inverse of packing.
|
|
2240
|
+
|
|
2150
2241
|
#### Negation (NOT)
|
|
2151
2242
|
|
|
2152
2243
|
Use `!` prefix to exclude keys matching a pattern:
|
|
@@ -2198,7 +2289,8 @@ A guard, filter or transform applies to the whole expression on its left:
|
|
|
2198
2289
|
an access path (on its last segment), a `!` expression (on what it
|
|
2199
2290
|
yields), or a parenthesized group, where it is shorthand for writing it on
|
|
2200
2291
|
every branch: `(a,b)=5` is `(a=5,b=5)` and `(a,b)|int` is `(a|int,b|int)`,
|
|
2201
|
-
which is also how `assemble` writes them.
|
|
2292
|
+
which is also how `assemble` writes them. A [packed](#packing) group
|
|
2293
|
+
yields one value, so `(a,b;)|sum` applies to that tuple once.
|
|
2202
2294
|
|
|
2203
2295
|
>>> d = {'a': 1, 'b': 2, 'c': 3}
|
|
2204
2296
|
>>> dotted.get(d, '!a&b,c') # ((!a)&b), c
|
|
@@ -3068,6 +3160,8 @@ decorator.
|
|
|
3068
3160
|
| `coalesce` | fallbacks... | The first of the value and the fallbacks that is not `None`: `\|coalesce:0` |
|
|
3069
3161
|
| `strip` | `chars`, `raises` | Strip whitespace or specified chars |
|
|
3070
3162
|
| `len` | `default` | Get length. Optional default if not sized: `\|len:0` |
|
|
3163
|
+
| `sum` | `start`, `raises` | Sum a container: `\|sum`, `\|sum:10`; over a pattern, `(a.*;)\|sum` |
|
|
3164
|
+
| `mean` | `raises` | Arithmetic mean of a container: `(a.*;)\|mean` |
|
|
3071
3165
|
| `lowercase` | `raises` | Convert string to lowercase |
|
|
3072
3166
|
| `uppercase` | `raises` | Convert string to uppercase |
|
|
3073
3167
|
| `add` | `rhs`, `raises` | Add value: `\|add:10` |
|
|
@@ -227,6 +227,11 @@ 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.49.0
|
|
231
|
+
- **`;` is now a reserved character**: it packs a group, `(a,b;)` (see
|
|
232
|
+
[Packing](#packing)). If you have keys containing a literal `;`, quote
|
|
233
|
+
them: `"a;b"`.
|
|
234
|
+
|
|
230
235
|
### v0.47.0
|
|
231
236
|
- **`!` binds tighter than guards, filters and transforms.** `!a=5` is now
|
|
232
237
|
`(!a)=5`, the values other than `a`'s that equal 5; the old meaning,
|
|
@@ -1344,7 +1349,8 @@ To chain through the items, use a pattern instead:
|
|
|
1344
1349
|
## Substitutions and References
|
|
1345
1350
|
|
|
1346
1351
|
All `$`-prefixed syntax falls into two categories: **substitutions** (resolved
|
|
1347
|
-
at replace time) and **references
|
|
1352
|
+
at replace time) and **references**, inline expressions evaluated during
|
|
1353
|
+
traversal.
|
|
1348
1354
|
|
|
1349
1355
|
| Syntax | Type | Resolved against |
|
|
1350
1356
|
|---|---|---|
|
|
@@ -1450,14 +1456,22 @@ the parsed result, so a path used many times can be compiled once:
|
|
|
1450
1456
|
<a id="references"></a>
|
|
1451
1457
|
### References
|
|
1452
1458
|
|
|
1453
|
-
A **reference
|
|
1454
|
-
|
|
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:
|
|
1455
1469
|
|
|
1456
1470
|
>>> data = {'config': {'field': 'name'}, 'name': 'Alice'}
|
|
1457
1471
|
>>> dotted.get(data, '$$(config.field)')
|
|
1458
1472
|
'Alice'
|
|
1459
1473
|
|
|
1460
|
-
Here `$$(config.field)`
|
|
1474
|
+
Here `$$(config.field)` evaluates to `'name'`, then looks up `data['name']`.
|
|
1461
1475
|
|
|
1462
1476
|
References work with all access types and in any position:
|
|
1463
1477
|
|
|
@@ -1554,6 +1568,12 @@ and so on:
|
|
|
1554
1568
|
>>> dotted.get(data, 'n|add:$$(config.offset)')
|
|
1555
1569
|
15
|
|
1556
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
|
+
|
|
1557
1577
|
>>> data = {'items': [{'price': 100, 'rate': 1.5}, {'price': 20, 'rate': 2}]}
|
|
1558
1578
|
>>> dotted.get(data, 'items[*].price|mul:$$(^^rate)')
|
|
1559
1579
|
(150.0, 40)
|
|
@@ -2100,6 +2120,77 @@ fallback as disjunction—first concrete path (last to first):
|
|
|
2100
2120
|
>>> dotted.update({'a': {}}, 'a(.x,.y)?', 99) # nothing matches → creates last (.y)
|
|
2101
2121
|
{'a': {'y': 99}}
|
|
2102
2122
|
|
|
2123
|
+
#### Packing
|
|
2124
|
+
|
|
2125
|
+
A group yields its matches one by one. A `;` just inside either paren
|
|
2126
|
+
**packs** them: the group yields one value, the tuple of what it would
|
|
2127
|
+
have yielded. `(a,b;)` and `(;a,b)` are the same pack, and the group
|
|
2128
|
+
keeps its own kind—`(a,b;)` packs the branches that match, `(a&b;)` packs
|
|
2129
|
+
both or nothing, `(a#,b;)` packs what the cut lets through, `(a,b;)?`
|
|
2130
|
+
packs the first match—and `(a.*;)` packs a one-branch group. Nothing
|
|
2131
|
+
matched packs to `()`:
|
|
2132
|
+
|
|
2133
|
+
>>> d = {'a': {'x': 1, 'y': 2}, 'b': 3}
|
|
2134
|
+
>>> dotted.get(d, '(a.*;)')
|
|
2135
|
+
(1, 2)
|
|
2136
|
+
>>> dotted.get(d, '(b,zz;)')
|
|
2137
|
+
(3,)
|
|
2138
|
+
>>> dotted.get(d, '(b&zz;)')
|
|
2139
|
+
()
|
|
2140
|
+
>>> dotted.get(d, '(zz.*;)')
|
|
2141
|
+
()
|
|
2142
|
+
|
|
2143
|
+
The tuple is one value, so a packed path is not a pattern: `get` returns
|
|
2144
|
+
the value bare, and a transform after the pack applies to the tuple once.
|
|
2145
|
+
This is how an aggregate over a pattern is written—the pack gathers, an
|
|
2146
|
+
ordinary [transform](#transforms) reduces:
|
|
2147
|
+
|
|
2148
|
+
>>> dotted.get(d, '(a.*;)|sum')
|
|
2149
|
+
3
|
|
2150
|
+
>>> dotted.get(d, '(a.*;)|mean')
|
|
2151
|
+
1.5
|
|
2152
|
+
>>> dotted.get(d, '(a.*;)|set|len')
|
|
2153
|
+
2
|
|
2154
|
+
>>> dotted.get({'a': 1, 'b': 2}, '(a,b;)|sum') # not (a|sum,b|sum)
|
|
2155
|
+
3
|
|
2156
|
+
|
|
2157
|
+
The group runs from the node the pack sits at, so a pack under a pattern
|
|
2158
|
+
gives one tuple per match—a group-by—while a pack around the whole path
|
|
2159
|
+
gathers across every level inside it:
|
|
2160
|
+
|
|
2161
|
+
>>> d = {'items': [{'p': [1, 2]}, {'p': [3]}, {'p': []}]}
|
|
2162
|
+
>>> dotted.get(d, 'items[*].(p[*];)|sum')
|
|
2163
|
+
(3, 3, 0)
|
|
2164
|
+
>>> dotted.get(d, '(items[*].p[*];)|sum')
|
|
2165
|
+
6
|
|
2166
|
+
|
|
2167
|
+
The path may continue into the tuple, and a guard compares it, with
|
|
2168
|
+
[guard transforms](#guard-transforms) before it:
|
|
2169
|
+
|
|
2170
|
+
>>> d = {'a': {'x': 1, 'y': 2}}
|
|
2171
|
+
>>> dotted.get(d, '(a.*;)[0]')
|
|
2172
|
+
1
|
|
2173
|
+
>>> dotted.get(d, '(a.*;)|sum>2')
|
|
2174
|
+
(1, 2)
|
|
2175
|
+
|
|
2176
|
+
A packed group may also lead a [filter](#filters) key, where it runs
|
|
2177
|
+
from each item and its tuple is the value compared—filter by an
|
|
2178
|
+
aggregate over the item:
|
|
2179
|
+
|
|
2180
|
+
>>> d = {'items': [{'p': [1, 2]}, {'p': [3]}, {'p': []}]}
|
|
2181
|
+
>>> dotted.get(d, 'items[(p[*];)|sum>2]')
|
|
2182
|
+
[{'p': [1, 2]}, {'p': [3]}]
|
|
2183
|
+
>>> dotted.get(d, 'items[*&(p[*];)|len=0]')
|
|
2184
|
+
({'p': []},)
|
|
2185
|
+
|
|
2186
|
+
A pack is its own path segment: `walk` and `expand` give it back as
|
|
2187
|
+
written, `(a.*;)|sum`. There is nothing under a tuple of matches to write
|
|
2188
|
+
to, so `update` and `remove` through a pack raise `ValueError`, and a pack
|
|
2189
|
+
cannot be negated. `;` is a reserved character; quote a key that contains
|
|
2190
|
+
one, `"a;b"`.
|
|
2191
|
+
|
|
2192
|
+
`(a.*;)[*]` yields the matches again: `[*]` is the inverse of packing.
|
|
2193
|
+
|
|
2103
2194
|
#### Negation (NOT)
|
|
2104
2195
|
|
|
2105
2196
|
Use `!` prefix to exclude keys matching a pattern:
|
|
@@ -2151,7 +2242,8 @@ A guard, filter or transform applies to the whole expression on its left:
|
|
|
2151
2242
|
an access path (on its last segment), a `!` expression (on what it
|
|
2152
2243
|
yields), or a parenthesized group, where it is shorthand for writing it on
|
|
2153
2244
|
every branch: `(a,b)=5` is `(a=5,b=5)` and `(a,b)|int` is `(a|int,b|int)`,
|
|
2154
|
-
which is also how `assemble` writes them.
|
|
2245
|
+
which is also how `assemble` writes them. A [packed](#packing) group
|
|
2246
|
+
yields one value, so `(a,b;)|sum` applies to that tuple once.
|
|
2155
2247
|
|
|
2156
2248
|
>>> d = {'a': 1, 'b': 2, 'c': 3}
|
|
2157
2249
|
>>> dotted.get(d, '!a&b,c') # ((!a)&b), c
|
|
@@ -3021,6 +3113,8 @@ decorator.
|
|
|
3021
3113
|
| `coalesce` | fallbacks... | The first of the value and the fallbacks that is not `None`: `\|coalesce:0` |
|
|
3022
3114
|
| `strip` | `chars`, `raises` | Strip whitespace or specified chars |
|
|
3023
3115
|
| `len` | `default` | Get length. Optional default if not sized: `\|len:0` |
|
|
3116
|
+
| `sum` | `start`, `raises` | Sum a container: `\|sum`, `\|sum:10`; over a pattern, `(a.*;)\|sum` |
|
|
3117
|
+
| `mean` | `raises` | Arithmetic mean of a container: `(a.*;)\|mean` |
|
|
3024
3118
|
| `lowercase` | `raises` | Convert string to lowercase |
|
|
3025
3119
|
| `uppercase` | `raises` | Convert string to uppercase |
|
|
3026
3120
|
| `add` | `rhs`, `raises` | Add value: `\|add:10` |
|
|
@@ -52,8 +52,9 @@ class FilterKey(base.MatchOp):
|
|
|
52
52
|
self.parts = tuple(self.args)
|
|
53
53
|
# Override args with tuple of parts for proper hashing
|
|
54
54
|
self.args = self.parts
|
|
55
|
-
# For simple keys (single part), expose value for backwards compat
|
|
56
|
-
|
|
55
|
+
# For simple keys (single part), expose value for backwards compat;
|
|
56
|
+
# a packed group, (a,b;), has none
|
|
57
|
+
self.value = getattr(self.parts[0], 'value', None) if len(self.parts) == 1 else None
|
|
57
58
|
|
|
58
59
|
def __repr__(self):
|
|
59
60
|
out = []
|
|
@@ -66,9 +67,20 @@ class FilterKey(base.MatchOp):
|
|
|
66
67
|
def __hash__(self):
|
|
67
68
|
return hash(self.parts)
|
|
68
69
|
|
|
70
|
+
def __eq__(self, other):
|
|
71
|
+
# spelled out: a compiled class with __hash__ alone compares by identity
|
|
72
|
+
return self.__class__ == other.__class__ and self.parts == other.parts
|
|
73
|
+
|
|
69
74
|
def is_dotted(self):
|
|
70
75
|
return len(self.parts) > 1
|
|
71
76
|
|
|
77
|
+
def is_packed(self):
|
|
78
|
+
"""
|
|
79
|
+
True if the key starts with a packed group, [(p[*];)|sum>2]: the
|
|
80
|
+
group runs from the item and its tuple is the key's value.
|
|
81
|
+
"""
|
|
82
|
+
return bool(self.parts) and hasattr(self.parts[0], 'gather')
|
|
83
|
+
|
|
72
84
|
def is_template(self):
|
|
73
85
|
"""
|
|
74
86
|
True if any part of the filter key contains a substitution reference.
|
|
@@ -81,24 +93,29 @@ class FilterKey(base.MatchOp):
|
|
|
81
93
|
"""
|
|
82
94
|
return _any_reference(self.parts)
|
|
83
95
|
|
|
84
|
-
def get_values(self, node):
|
|
96
|
+
def get_values(self, node, settings=base.SETTINGS):
|
|
85
97
|
"""
|
|
86
98
|
Get all values from node matching this key, traversing dotted path if needed.
|
|
87
|
-
Supports slot parts (e.g. tags[*]) to yield each element of a list for matching
|
|
99
|
+
Supports slot parts (e.g. tags[*]) to yield each element of a list for matching,
|
|
100
|
+
and a leading packed group, (p[*];), whose tuple is the value; settings
|
|
101
|
+
are the traversal's, for references inside the group.
|
|
88
102
|
Yields (value, True) for each match, or (None, False) if no matches.
|
|
89
103
|
"""
|
|
90
|
-
yield from self._get_values(node, list(self.parts))
|
|
104
|
+
yield from self._get_values(node, list(self.parts), settings)
|
|
91
105
|
|
|
92
|
-
def _get_values(self, node, parts):
|
|
106
|
+
def _get_values(self, node, parts, settings=base.SETTINGS):
|
|
93
107
|
if not parts:
|
|
94
108
|
yield node, True
|
|
95
109
|
return
|
|
96
110
|
part = parts[0]
|
|
97
111
|
rest = parts[1:]
|
|
112
|
+
if hasattr(part, 'gather'):
|
|
113
|
+
yield from self._get_values(part.gather(node, settings), rest, settings)
|
|
114
|
+
return
|
|
98
115
|
if isinstance(part, Slot):
|
|
99
116
|
try:
|
|
100
117
|
for v in part.values(node):
|
|
101
|
-
yield from self._get_values(v, rest)
|
|
118
|
+
yield from self._get_values(v, rest, settings)
|
|
102
119
|
except (TypeError, AttributeError):
|
|
103
120
|
yield None, False
|
|
104
121
|
return
|
|
@@ -106,7 +123,7 @@ class FilterKey(base.MatchOp):
|
|
|
106
123
|
try:
|
|
107
124
|
s = part.slice(node)
|
|
108
125
|
val = node[s]
|
|
109
|
-
yield from self._get_values(val, rest)
|
|
126
|
+
yield from self._get_values(val, rest, settings)
|
|
110
127
|
except (TypeError, AttributeError, KeyError, IndexError):
|
|
111
128
|
yield None, False
|
|
112
129
|
return
|
|
@@ -127,7 +144,7 @@ class FilterKey(base.MatchOp):
|
|
|
127
144
|
found = False
|
|
128
145
|
for km in part.matches(node.keys()):
|
|
129
146
|
child = node[km]
|
|
130
|
-
yield from self._get_values(child, rest)
|
|
147
|
+
yield from self._get_values(child, rest, settings)
|
|
131
148
|
found = True
|
|
132
149
|
break
|
|
133
150
|
if not found:
|
|
@@ -145,9 +162,9 @@ class FilterKey(base.MatchOp):
|
|
|
145
162
|
"""
|
|
146
163
|
For simple keys, delegate to the inner part's matches
|
|
147
164
|
"""
|
|
148
|
-
if not self.is_dotted():
|
|
165
|
+
if not self.is_dotted() and not self.is_packed():
|
|
149
166
|
return self.parts[0].matches(keys)
|
|
150
|
-
return ()
|
|
167
|
+
return iter(())
|
|
151
168
|
|
|
152
169
|
def matchable(self, op):
|
|
153
170
|
"""
|
|
@@ -214,6 +231,10 @@ class FilterKeyValue(FilterOp):
|
|
|
214
231
|
def __hash__(self):
|
|
215
232
|
return hash((self._eq_str, self.key, self.val, self.guard_transforms))
|
|
216
233
|
|
|
234
|
+
def __eq__(self, other):
|
|
235
|
+
# spelled out: a compiled class with __hash__ alone compares by identity
|
|
236
|
+
return self.__class__ == other.__class__ and (self._eq_str, self.key, self.val, self.guard_transforms) == (other._eq_str, other.key, other.val, other.guard_transforms)
|
|
237
|
+
|
|
217
238
|
def __repr__(self):
|
|
218
239
|
t_str = ''.join('|' + t.operator() for t in self.guard_transforms) if self.guard_transforms else ''
|
|
219
240
|
return f'{self.key}{t_str}{self._eq_str}{self.val}'
|
|
@@ -252,11 +273,11 @@ class FilterKeyValue(FilterOp):
|
|
|
252
273
|
self.val sees the value as ^, node as ^^ and node's tracked parents
|
|
253
274
|
above that; one that does not resolve matches nothing.
|
|
254
275
|
"""
|
|
255
|
-
if not hasattr(node, 'keys'):
|
|
276
|
+
if not hasattr(node, 'keys') and not self.key.is_packed():
|
|
256
277
|
return False
|
|
257
278
|
from . import results
|
|
258
279
|
parents = (node,) + (settings.parents or ())
|
|
259
|
-
for val, found in self.key.get_values(node):
|
|
280
|
+
for val, found in self.key.get_values(node, settings):
|
|
260
281
|
if not found:
|
|
261
282
|
continue
|
|
262
283
|
try:
|
|
@@ -292,9 +313,13 @@ class FilterKeyValue(FilterOp):
|
|
|
292
313
|
return None
|
|
293
314
|
if not self.key.matchable(op.key) or not self.val.matchable(op.val):
|
|
294
315
|
return None
|
|
295
|
-
|
|
316
|
+
if self.key.is_packed():
|
|
317
|
+
# a packed key has no single value: it matches an equal key
|
|
318
|
+
mk = self.key.match(op.key)
|
|
319
|
+
else:
|
|
320
|
+
mk = next(self.key.matches((op.key.value,)), base.marker)
|
|
296
321
|
mv = next(self.val.matches((op.val.value,)), base.marker)
|
|
297
|
-
if base.marker in (mk, mv):
|
|
322
|
+
if mk is None or base.marker in (mk, mv):
|
|
298
323
|
return None
|
|
299
324
|
return type(op)(op.key, op.val)
|
|
300
325
|
|
|
@@ -384,6 +409,10 @@ class FilterGroup(FilterOp):
|
|
|
384
409
|
def __hash__(self):
|
|
385
410
|
return hash(self.inner)
|
|
386
411
|
|
|
412
|
+
def __eq__(self, other):
|
|
413
|
+
# spelled out: a compiled class with __hash__ alone compares by identity
|
|
414
|
+
return self.__class__ == other.__class__ and self.inner == other.inner
|
|
415
|
+
|
|
387
416
|
def __repr__(self):
|
|
388
417
|
return f'({self.inner})'
|
|
389
418
|
|
|
@@ -434,6 +463,10 @@ class FilterAnd(FilterOp):
|
|
|
434
463
|
def __hash__(self):
|
|
435
464
|
return hash(self.filters)
|
|
436
465
|
|
|
466
|
+
def __eq__(self, other):
|
|
467
|
+
# spelled out: a compiled class with __hash__ alone compares by identity
|
|
468
|
+
return self.__class__ == other.__class__ and self.filters == other.filters
|
|
469
|
+
|
|
437
470
|
def __repr__(self):
|
|
438
471
|
return '&'.join(str(f) for f in self.filters)
|
|
439
472
|
|
|
@@ -492,6 +525,10 @@ class FilterOr(FilterOp):
|
|
|
492
525
|
def __hash__(self):
|
|
493
526
|
return hash(self.filters)
|
|
494
527
|
|
|
528
|
+
def __eq__(self, other):
|
|
529
|
+
# spelled out: a compiled class with __hash__ alone compares by identity
|
|
530
|
+
return self.__class__ == other.__class__ and self.filters == other.filters
|
|
531
|
+
|
|
495
532
|
def __repr__(self):
|
|
496
533
|
return ','.join(str(f) for f in self.filters)
|
|
497
534
|
|
|
@@ -538,6 +575,10 @@ class FilterKeyValueFirst(FilterOp):
|
|
|
538
575
|
def __hash__(self):
|
|
539
576
|
return hash(self.inner)
|
|
540
577
|
|
|
578
|
+
def __eq__(self, other):
|
|
579
|
+
# spelled out: a compiled class with __hash__ alone compares by identity
|
|
580
|
+
return self.__class__ == other.__class__ and self.inner == other.inner
|
|
581
|
+
|
|
541
582
|
def __repr__(self):
|
|
542
583
|
return f'{self.inner}?'
|
|
543
584
|
|
|
@@ -598,6 +639,10 @@ class FilterNot(FilterOp):
|
|
|
598
639
|
def __hash__(self):
|
|
599
640
|
return hash(('not', self.inner))
|
|
600
641
|
|
|
642
|
+
def __eq__(self, other):
|
|
643
|
+
# spelled out: a compiled class with __hash__ alone compares by identity
|
|
644
|
+
return self.__class__ == other.__class__ and self.inner == other.inner
|
|
645
|
+
|
|
601
646
|
def __repr__(self):
|
|
602
647
|
return f'!{self.inner}'
|
|
603
648
|
|
|
@@ -43,6 +43,8 @@ def inner_not_action(t):
|
|
|
43
43
|
item = t[0]
|
|
44
44
|
if isinstance(item, (list, tuple, pp.ParseResults)) and len(item) == 1:
|
|
45
45
|
item = item[0] # an op_seq of one op
|
|
46
|
+
if isinstance(item, groups.Pack):
|
|
47
|
+
raise ValueError(f'a packed group cannot be negated: !{item!r}')
|
|
46
48
|
if isinstance(item, groups.OpGroupOr):
|
|
47
49
|
# a parenthesized single path, !(a.b): the group adds nothing, so
|
|
48
50
|
# !(a.b) is the same op as !a.b
|
|
@@ -92,8 +94,8 @@ def inner_to_opgroup(parsed_result):
|
|
|
92
94
|
items = list(parsed_result)
|
|
93
95
|
if not items:
|
|
94
96
|
return groups.OpGroupOr()
|
|
95
|
-
# If there's a single OpGroup result, use it directly
|
|
96
|
-
if len(items) == 1 and isinstance(items[0], groups.OpGroup):
|
|
97
|
+
# If there's a single OpGroup (or packed group) result, use it directly
|
|
98
|
+
if len(items) == 1 and isinstance(items[0], (groups.OpGroup, groups.Pack)):
|
|
97
99
|
inner = items[0]
|
|
98
100
|
# Only unwrap redundant OpGroupOr wrapping a single OpGroupAnd/OpGroupNot
|
|
99
101
|
if isinstance(inner, groups.OpGroupOr):
|
|
@@ -220,7 +222,7 @@ none = (pp.Literal('None') | pp.Literal('null')).set_parse_action(matchers.NoneV
|
|
|
220
222
|
true = (pp.Literal('True') | pp.Literal('true')).set_parse_action(matchers.Boolean)
|
|
221
223
|
false = (pp.Literal('False') | pp.Literal('false')).set_parse_action(matchers.Boolean)
|
|
222
224
|
|
|
223
|
-
reserved = '.[]*:|+?/=,@&()!~#{}
|
|
225
|
+
reserved = '.[]*:|+?/=,@&()!~#{}$<>;' # {} for container syntax, $ for substitution, <> for comparisons, ; for packing
|
|
224
226
|
breserved = ''.join('\\' + i for i in reserved)
|
|
225
227
|
tilde = L('~')
|
|
226
228
|
|
|
@@ -601,7 +603,10 @@ key = _concat_key | _key_atom
|
|
|
601
603
|
_filter_key_part = string | _common_pats | numeric_extended | non_integer | numeric_key | word
|
|
602
604
|
filter_key_slice = (lb + slice + rb).set_parse_action(lambda t: access.Slice(*t))
|
|
603
605
|
filter_key_slot = (lb + (_commons | numeric_extended | numeric_slot) + rb).set_parse_action(lambda t: access.Slot(t[0]))
|
|
604
|
-
|
|
606
|
+
# A packed group may lead a filter key, [(p[*];)|sum>2]; set below, once
|
|
607
|
+
# the group grammar exists.
|
|
608
|
+
_filter_key_group = pp.Forward()
|
|
609
|
+
_filter_key_segment = _filter_key_group | _filter_key_part | filter_key_slice | filter_key_slot
|
|
605
610
|
filter_key = pp.Group(
|
|
606
611
|
_filter_key_segment + ZM(filter_key_slice | filter_key_slot | (dot + _filter_key_part))
|
|
607
612
|
).set_parse_action(flt.FilterKey)
|
|
@@ -729,6 +734,10 @@ slicefilter = (lb + filters + ZM(amp + filters) + rb).set_parse_action(access.Sl
|
|
|
729
734
|
softcut_marker = L('##')
|
|
730
735
|
cut_marker = softcut_marker | L('#')
|
|
731
736
|
|
|
737
|
+
# Pack marker: a ; just inside either paren, (a,b;) or (;a,b), packs the
|
|
738
|
+
# group's matches into one tuple (see groups.Pack).
|
|
739
|
+
pack_marker = L(';')
|
|
740
|
+
|
|
732
741
|
# Slot grouping: [(*&filter, +)] for disjunction inside slots; [(*&filter#, +)] for cut
|
|
733
742
|
def _slot_item_action(t):
|
|
734
743
|
_, filters, _, _, args = _classify_tokens(list(t))
|
|
@@ -754,8 +763,12 @@ def _grouped_with_type_restriction(parsed_result, first=False):
|
|
|
754
763
|
"""
|
|
755
764
|
Parse action for grouped expressions with optional type restriction.
|
|
756
765
|
Last token may be a TypeSpec; if so, wrap the OpGroup in TypeRestriction.
|
|
766
|
+
A ; token at either end packs the group (see groups.Pack); the type
|
|
767
|
+
restriction then guards the tuple.
|
|
757
768
|
"""
|
|
758
769
|
items = list(parsed_result)
|
|
770
|
+
packed = any(isinstance(i, str) and i == ';' for i in items)
|
|
771
|
+
items = [i for i in items if not (isinstance(i, str) and i == ';')]
|
|
759
772
|
tr = None
|
|
760
773
|
if items and isinstance(items[-1], utypes.TypeSpec):
|
|
761
774
|
tr = items.pop()
|
|
@@ -763,32 +776,50 @@ def _grouped_with_type_restriction(parsed_result, first=False):
|
|
|
763
776
|
grp = inner_to_opgroup_first(items)
|
|
764
777
|
else:
|
|
765
778
|
grp = inner_to_opgroup(items)
|
|
779
|
+
if packed:
|
|
780
|
+
grp = groups.Pack(grp)
|
|
766
781
|
if tr:
|
|
767
782
|
grp = tr.wrap(grp)
|
|
768
783
|
return grp
|
|
769
784
|
|
|
770
|
-
|
|
785
|
+
_packable = lambda expr: Opt(pack_marker) + expr + Opt(pack_marker)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _filter_key_pack_action(s, loc, t):
|
|
789
|
+
"""
|
|
790
|
+
Parse action for a group leading a filter key: it must be packed,
|
|
791
|
+
the tuple being the value compared; an unpacked group has no single
|
|
792
|
+
value there, so it fails to parse as it always did.
|
|
793
|
+
"""
|
|
794
|
+
grp = _grouped_with_type_restriction(t)
|
|
795
|
+
if not isinstance(grp, groups.Pack):
|
|
796
|
+
raise pp.ParseException(s, loc, 'a group in a filter key must be packed, (a,b;)')
|
|
797
|
+
return grp
|
|
798
|
+
|
|
799
|
+
_filter_key_group <<= (lparen + _packable(inner_expr) + rparen).set_parse_action(_filter_key_pack_action)
|
|
800
|
+
|
|
801
|
+
inner_grouped = (lparen + _packable(inner_expr) + rparen).set_parse_action(
|
|
771
802
|
lambda t: _grouped_with_type_restriction(t))
|
|
772
|
-
inner_grouped_first = (lparen + inner_expr + rparen + S('?')).set_parse_action(
|
|
803
|
+
inner_grouped_first = (lparen + _packable(inner_expr) + rparen + S('?')).set_parse_action(
|
|
773
804
|
lambda t: _grouped_with_type_restriction(t, first=True))
|
|
774
805
|
# Explicit-op grouped: used mid-path where bare-key inference is not allowed.
|
|
775
806
|
# Uses a separate precedence tower (_explicit_inner_expr) that only allows
|
|
776
807
|
# op_seqs built from _op_seq_cont (no bare keys even in first position).
|
|
777
808
|
# The FollowedBy is a fast-fail optimization — if the first char after ( can't
|
|
778
809
|
# start an explicit branch, don't even try the tower.
|
|
779
|
-
_explicit_op_start = pp.FollowedBy(pp.Char('.@[!~('))
|
|
810
|
+
_explicit_op_start = pp.FollowedBy(pp.Char('.@[!~(;'))
|
|
780
811
|
_explicit_inner_expr = pp.Forward()
|
|
781
|
-
_inner_grouped_explicit = (lparen + _explicit_op_start + _explicit_inner_expr + rparen).set_parse_action(
|
|
812
|
+
_inner_grouped_explicit = (lparen + _explicit_op_start + _packable(_explicit_inner_expr) + rparen).set_parse_action(
|
|
782
813
|
lambda t: _grouped_with_type_restriction(t))
|
|
783
|
-
_inner_grouped_explicit_first = (lparen + _explicit_op_start + _explicit_inner_expr + rparen + S('?')).set_parse_action(
|
|
814
|
+
_inner_grouped_explicit_first = (lparen + _explicit_op_start + _packable(_explicit_inner_expr) + rparen + S('?')).set_parse_action(
|
|
784
815
|
lambda t: _grouped_with_type_restriction(t, first=True))
|
|
785
816
|
# Bare-key grouped: first branch does NOT start with an access op.
|
|
786
817
|
# Used with prefix shorthand (.(a,b), @(a,b)) where the prefix distributes.
|
|
787
818
|
# Allows ! and ~ (modifiers) but rejects . @ [ ( (access ops / nested groups).
|
|
788
819
|
_bare_key_start = ~pp.FollowedBy(pp.Char('.@[('))
|
|
789
|
-
_inner_grouped_bare = (lparen + _bare_key_start + inner_expr + rparen).set_parse_action(
|
|
820
|
+
_inner_grouped_bare = (lparen + _bare_key_start + _packable(inner_expr) + rparen).set_parse_action(
|
|
790
821
|
lambda t: _grouped_with_type_restriction(t))
|
|
791
|
-
_inner_grouped_bare_first = (lparen + _bare_key_start + inner_expr + rparen + S('?')).set_parse_action(
|
|
822
|
+
_inner_grouped_bare_first = (lparen + _bare_key_start + _packable(inner_expr) + rparen + S('?')).set_parse_action(
|
|
792
823
|
lambda t: _grouped_with_type_restriction(t, first=True))
|
|
793
824
|
|
|
794
825
|
# Recursive operator: ** (recursive wildcard) and *pattern (recursive chain-following)
|
|
@@ -92,6 +92,12 @@ class OpGroup(base.TraversalOp):
|
|
|
92
92
|
def __hash__(self):
|
|
93
93
|
return hash(self.branches)
|
|
94
94
|
|
|
95
|
+
def __eq__(self, other):
|
|
96
|
+
# spelled out, not inherited: a compiled class that defines
|
|
97
|
+
# __hash__ alone compares by identity (CPython inherits
|
|
98
|
+
# tp_richcompare only together with tp_hash)
|
|
99
|
+
return self.__class__ == other.__class__ and self.branches == other.branches
|
|
100
|
+
|
|
95
101
|
def is_pattern(self):
|
|
96
102
|
return True
|
|
97
103
|
|
|
@@ -841,6 +847,117 @@ class OpGroupNot(OpGroup):
|
|
|
841
847
|
# =============================================================================
|
|
842
848
|
|
|
843
849
|
|
|
850
|
+
class Pack(base.TraversalOp):
|
|
851
|
+
"""
|
|
852
|
+
A packed group, (a,b;) or (;a,b): the group's matches as one tuple.
|
|
853
|
+
The group keeps its own kind, so (a,b;) packs whatever branches
|
|
854
|
+
match, (a&b;) packs both or nothing, (a#,b;) packs what the cut
|
|
855
|
+
lets through, and (a.*;) packs a one-branch group; no matches pack
|
|
856
|
+
to (). Yielding one value, it is not a pattern, and a transform
|
|
857
|
+
after it applies to the tuple once: (a.*;)|sum. The group is run
|
|
858
|
+
from the node the pack sits at, so a[*].(b.*;) packs per a. Writes
|
|
859
|
+
through a pack raise. Not a Wrap: the pack is the op, the group
|
|
860
|
+
its argument, so an unwrapping stops here.
|
|
861
|
+
"""
|
|
862
|
+
|
|
863
|
+
def __init__(self, inner):
|
|
864
|
+
super().__init__(inner)
|
|
865
|
+
self.inner = inner
|
|
866
|
+
|
|
867
|
+
def __repr__(self):
|
|
868
|
+
return self.operator(top=True)
|
|
869
|
+
|
|
870
|
+
def is_template(self):
|
|
871
|
+
return self.inner.is_template()
|
|
872
|
+
|
|
873
|
+
def is_reference(self):
|
|
874
|
+
return self.inner.is_reference()
|
|
875
|
+
|
|
876
|
+
def matchable(self, op):
|
|
877
|
+
return op == self
|
|
878
|
+
|
|
879
|
+
def match(self, op, specials=False):
|
|
880
|
+
"""
|
|
881
|
+
A pack denotes only itself: it matches an equal pack.
|
|
882
|
+
"""
|
|
883
|
+
if not self.matchable(op):
|
|
884
|
+
return None
|
|
885
|
+
return base.MatchResult(self)
|
|
886
|
+
|
|
887
|
+
def do_match(self, rest_pats, path_ops, partial):
|
|
888
|
+
"""
|
|
889
|
+
One segment, matched by equality (see match).
|
|
890
|
+
"""
|
|
891
|
+
if not path_ops or not self.match(path_ops[0]):
|
|
892
|
+
return None
|
|
893
|
+
rest = base.match_ops(rest_pats, path_ops[1:], partial)
|
|
894
|
+
if rest is None:
|
|
895
|
+
return None
|
|
896
|
+
return [(self, False)] + rest
|
|
897
|
+
|
|
898
|
+
def operator(self, top=False):
|
|
899
|
+
"""
|
|
900
|
+
The group's text with the marker before its closing paren:
|
|
901
|
+
(a,b;), (a,b;)?.
|
|
902
|
+
"""
|
|
903
|
+
s = self.inner.operator(top)
|
|
904
|
+
i = s.rindex(')')
|
|
905
|
+
return s[:i] + ';' + s[i:]
|
|
906
|
+
|
|
907
|
+
def is_pattern(self):
|
|
908
|
+
return False
|
|
909
|
+
|
|
910
|
+
def as_attrs(self):
|
|
911
|
+
return Pack(self.inner.as_attrs())
|
|
912
|
+
|
|
913
|
+
def resolve(self, bindings, partial=False):
|
|
914
|
+
new_inner = self.inner.resolve(bindings, partial)
|
|
915
|
+
if new_inner is self.inner:
|
|
916
|
+
return self
|
|
917
|
+
return Pack(new_inner)
|
|
918
|
+
|
|
919
|
+
def concrete(self, val):
|
|
920
|
+
"""
|
|
921
|
+
A pack is its own path segment: a walked path reads back as
|
|
922
|
+
written, (a.*;).
|
|
923
|
+
"""
|
|
924
|
+
return self
|
|
925
|
+
|
|
926
|
+
def gather(self, node, settings=base.SETTINGS):
|
|
927
|
+
"""
|
|
928
|
+
The tuple of the group's matches from node, stopping at a cut.
|
|
929
|
+
"""
|
|
930
|
+
return tuple(engine.values((self.inner,), node, settings=settings))
|
|
931
|
+
|
|
932
|
+
def items(self, node, settings=base.SETTINGS, filtered=True):
|
|
933
|
+
yield None, self.gather(node, settings)
|
|
934
|
+
|
|
935
|
+
def keys(self, node, settings=base.SETTINGS):
|
|
936
|
+
yield None
|
|
937
|
+
|
|
938
|
+
def values(self, node, settings=base.SETTINGS):
|
|
939
|
+
yield self.gather(node, settings)
|
|
940
|
+
|
|
941
|
+
def is_empty(self, node):
|
|
942
|
+
return False
|
|
943
|
+
|
|
944
|
+
def push_children(self, stack, frame, paths):
|
|
945
|
+
"""
|
|
946
|
+
One frame, the tuple, with this pack as its path segment; the
|
|
947
|
+
node the group ran from is its parent for ^^ references.
|
|
948
|
+
"""
|
|
949
|
+
packed = self.gather(frame.node, frame.settings)
|
|
950
|
+
cp = frame.prefix + (self,) if paths else frame.prefix
|
|
951
|
+
stack.push(base.Frame(frame.ops, packed, cp, settings=frame.settings.below(frame.node)))
|
|
952
|
+
return ()
|
|
953
|
+
|
|
954
|
+
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, settings=base.SETTINGS):
|
|
955
|
+
raise ValueError(f'cannot update through a packed group: {self!r}')
|
|
956
|
+
|
|
957
|
+
def do_remove(self, ops, node, val, nop, settings=base.SETTINGS):
|
|
958
|
+
raise ValueError(f'cannot remove through a packed group: {self!r}')
|
|
959
|
+
|
|
960
|
+
|
|
844
961
|
def _attr_branch(branch):
|
|
845
962
|
"""
|
|
846
963
|
Convert leading Key to Attr in a branch tuple.
|
|
@@ -336,6 +336,8 @@ class PostgresMixin:
|
|
|
336
336
|
op = op.inner
|
|
337
337
|
if isinstance(op, wrappers.TypeRestriction):
|
|
338
338
|
raise TranslationError('type restriction not supported')
|
|
339
|
+
if isinstance(op.most_inner, _groups.Pack):
|
|
340
|
+
raise TranslationError('packed group not supported')
|
|
339
341
|
# Groups: each branch is walked independently, so dispatch first
|
|
340
342
|
# (OpGroup has is_pattern=True but shouldn't take the pattern-mode
|
|
341
343
|
# path wholesale).
|
|
@@ -459,6 +461,8 @@ class PostgresMixin:
|
|
|
459
461
|
return self._op_to_jsonpath(op.inner, jvars)
|
|
460
462
|
if isinstance(op, wrappers.TypeRestriction):
|
|
461
463
|
raise TranslationError('type restriction not supported')
|
|
464
|
+
if isinstance(op.most_inner, _groups.Pack):
|
|
465
|
+
raise TranslationError('packed group not supported')
|
|
462
466
|
# NB: Slot is a subclass of Key, so check Slot first.
|
|
463
467
|
if isinstance(op, access.Slot):
|
|
464
468
|
return self._key_slot_to_jsonpath(op, is_key=False), None
|
|
@@ -674,6 +678,8 @@ class PostgresMixin:
|
|
|
674
678
|
if isinstance(key, _filters.FilterKey):
|
|
675
679
|
parts = ['@']
|
|
676
680
|
for p in key.parts:
|
|
681
|
+
if isinstance(p, _groups.Pack):
|
|
682
|
+
raise TranslationError('packed group in filter key not supported')
|
|
677
683
|
if isinstance(p, access.Slot):
|
|
678
684
|
m = p.op
|
|
679
685
|
if isinstance(m, matchers.Wildcard):
|
|
@@ -3,6 +3,7 @@ Default transforms
|
|
|
3
3
|
"""
|
|
4
4
|
import decimal
|
|
5
5
|
import math
|
|
6
|
+
import statistics
|
|
6
7
|
from .results import Dotted
|
|
7
8
|
|
|
8
9
|
|
|
@@ -502,6 +503,45 @@ def transform_not_in(val, rhs, *modes):
|
|
|
502
503
|
return val
|
|
503
504
|
|
|
504
505
|
|
|
506
|
+
@transform('sum')
|
|
507
|
+
def transform_sum(val, start=None, *modes):
|
|
508
|
+
"""
|
|
509
|
+
Sum the elements of val
|
|
510
|
+
<dotted>|sum sum(val)
|
|
511
|
+
<dotted>|sum:<start> sum(val, <start>)
|
|
512
|
+
<dotted>|sum::raises sum(val) or raises
|
|
513
|
+
>>> import dotted
|
|
514
|
+
>>> dotted.get({'a': {'x': 1, 'y': 2}}, '(a.*;)|sum')
|
|
515
|
+
3
|
|
516
|
+
"""
|
|
517
|
+
try:
|
|
518
|
+
if start is None:
|
|
519
|
+
return sum(val)
|
|
520
|
+
return sum(val, start)
|
|
521
|
+
except TypeError:
|
|
522
|
+
if 'raises' in modes:
|
|
523
|
+
raise
|
|
524
|
+
return val
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
@transform('mean')
|
|
528
|
+
def transform_mean(val, *modes):
|
|
529
|
+
"""
|
|
530
|
+
Arithmetic mean of the elements of val
|
|
531
|
+
<dotted>|mean statistics.mean(val)
|
|
532
|
+
<dotted>|mean:raises statistics.mean(val) or raises
|
|
533
|
+
>>> import dotted
|
|
534
|
+
>>> dotted.get({'a': {'x': 1, 'y': 2}}, '(a.*;)|mean')
|
|
535
|
+
1.5
|
|
536
|
+
"""
|
|
537
|
+
try:
|
|
538
|
+
return statistics.mean(val)
|
|
539
|
+
except (TypeError, statistics.StatisticsError):
|
|
540
|
+
if 'raises' in modes:
|
|
541
|
+
raise
|
|
542
|
+
return val
|
|
543
|
+
|
|
544
|
+
|
|
505
545
|
@transform('list')
|
|
506
546
|
def transform_list(val, *modes):
|
|
507
547
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.49.1
|
|
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
|
|
@@ -274,6 +274,11 @@ Several Python libraries handle nested data access. Here's how dotted compares:
|
|
|
274
274
|
<a id="breaking-changes"></a>
|
|
275
275
|
## Breaking Changes
|
|
276
276
|
|
|
277
|
+
### v0.49.0
|
|
278
|
+
- **`;` is now a reserved character**: it packs a group, `(a,b;)` (see
|
|
279
|
+
[Packing](#packing)). If you have keys containing a literal `;`, quote
|
|
280
|
+
them: `"a;b"`.
|
|
281
|
+
|
|
277
282
|
### v0.47.0
|
|
278
283
|
- **`!` binds tighter than guards, filters and transforms.** `!a=5` is now
|
|
279
284
|
`(!a)=5`, the values other than `a`'s that equal 5; the old meaning,
|
|
@@ -1391,7 +1396,8 @@ To chain through the items, use a pattern instead:
|
|
|
1391
1396
|
## Substitutions and References
|
|
1392
1397
|
|
|
1393
1398
|
All `$`-prefixed syntax falls into two categories: **substitutions** (resolved
|
|
1394
|
-
at replace time) and **references
|
|
1399
|
+
at replace time) and **references**, inline expressions evaluated during
|
|
1400
|
+
traversal.
|
|
1395
1401
|
|
|
1396
1402
|
| Syntax | Type | Resolved against |
|
|
1397
1403
|
|---|---|---|
|
|
@@ -1497,14 +1503,22 @@ the parsed result, so a path used many times can be compiled once:
|
|
|
1497
1503
|
<a id="references"></a>
|
|
1498
1504
|
### References
|
|
1499
1505
|
|
|
1500
|
-
A **reference
|
|
1501
|
-
|
|
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:
|
|
1502
1516
|
|
|
1503
1517
|
>>> data = {'config': {'field': 'name'}, 'name': 'Alice'}
|
|
1504
1518
|
>>> dotted.get(data, '$$(config.field)')
|
|
1505
1519
|
'Alice'
|
|
1506
1520
|
|
|
1507
|
-
Here `$$(config.field)`
|
|
1521
|
+
Here `$$(config.field)` evaluates to `'name'`, then looks up `data['name']`.
|
|
1508
1522
|
|
|
1509
1523
|
References work with all access types and in any position:
|
|
1510
1524
|
|
|
@@ -1601,6 +1615,12 @@ and so on:
|
|
|
1601
1615
|
>>> dotted.get(data, 'n|add:$$(config.offset)')
|
|
1602
1616
|
15
|
|
1603
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
|
+
|
|
1604
1624
|
>>> data = {'items': [{'price': 100, 'rate': 1.5}, {'price': 20, 'rate': 2}]}
|
|
1605
1625
|
>>> dotted.get(data, 'items[*].price|mul:$$(^^rate)')
|
|
1606
1626
|
(150.0, 40)
|
|
@@ -2147,6 +2167,77 @@ fallback as disjunction—first concrete path (last to first):
|
|
|
2147
2167
|
>>> dotted.update({'a': {}}, 'a(.x,.y)?', 99) # nothing matches → creates last (.y)
|
|
2148
2168
|
{'a': {'y': 99}}
|
|
2149
2169
|
|
|
2170
|
+
#### Packing
|
|
2171
|
+
|
|
2172
|
+
A group yields its matches one by one. A `;` just inside either paren
|
|
2173
|
+
**packs** them: the group yields one value, the tuple of what it would
|
|
2174
|
+
have yielded. `(a,b;)` and `(;a,b)` are the same pack, and the group
|
|
2175
|
+
keeps its own kind—`(a,b;)` packs the branches that match, `(a&b;)` packs
|
|
2176
|
+
both or nothing, `(a#,b;)` packs what the cut lets through, `(a,b;)?`
|
|
2177
|
+
packs the first match—and `(a.*;)` packs a one-branch group. Nothing
|
|
2178
|
+
matched packs to `()`:
|
|
2179
|
+
|
|
2180
|
+
>>> d = {'a': {'x': 1, 'y': 2}, 'b': 3}
|
|
2181
|
+
>>> dotted.get(d, '(a.*;)')
|
|
2182
|
+
(1, 2)
|
|
2183
|
+
>>> dotted.get(d, '(b,zz;)')
|
|
2184
|
+
(3,)
|
|
2185
|
+
>>> dotted.get(d, '(b&zz;)')
|
|
2186
|
+
()
|
|
2187
|
+
>>> dotted.get(d, '(zz.*;)')
|
|
2188
|
+
()
|
|
2189
|
+
|
|
2190
|
+
The tuple is one value, so a packed path is not a pattern: `get` returns
|
|
2191
|
+
the value bare, and a transform after the pack applies to the tuple once.
|
|
2192
|
+
This is how an aggregate over a pattern is written—the pack gathers, an
|
|
2193
|
+
ordinary [transform](#transforms) reduces:
|
|
2194
|
+
|
|
2195
|
+
>>> dotted.get(d, '(a.*;)|sum')
|
|
2196
|
+
3
|
|
2197
|
+
>>> dotted.get(d, '(a.*;)|mean')
|
|
2198
|
+
1.5
|
|
2199
|
+
>>> dotted.get(d, '(a.*;)|set|len')
|
|
2200
|
+
2
|
|
2201
|
+
>>> dotted.get({'a': 1, 'b': 2}, '(a,b;)|sum') # not (a|sum,b|sum)
|
|
2202
|
+
3
|
|
2203
|
+
|
|
2204
|
+
The group runs from the node the pack sits at, so a pack under a pattern
|
|
2205
|
+
gives one tuple per match—a group-by—while a pack around the whole path
|
|
2206
|
+
gathers across every level inside it:
|
|
2207
|
+
|
|
2208
|
+
>>> d = {'items': [{'p': [1, 2]}, {'p': [3]}, {'p': []}]}
|
|
2209
|
+
>>> dotted.get(d, 'items[*].(p[*];)|sum')
|
|
2210
|
+
(3, 3, 0)
|
|
2211
|
+
>>> dotted.get(d, '(items[*].p[*];)|sum')
|
|
2212
|
+
6
|
|
2213
|
+
|
|
2214
|
+
The path may continue into the tuple, and a guard compares it, with
|
|
2215
|
+
[guard transforms](#guard-transforms) before it:
|
|
2216
|
+
|
|
2217
|
+
>>> d = {'a': {'x': 1, 'y': 2}}
|
|
2218
|
+
>>> dotted.get(d, '(a.*;)[0]')
|
|
2219
|
+
1
|
|
2220
|
+
>>> dotted.get(d, '(a.*;)|sum>2')
|
|
2221
|
+
(1, 2)
|
|
2222
|
+
|
|
2223
|
+
A packed group may also lead a [filter](#filters) key, where it runs
|
|
2224
|
+
from each item and its tuple is the value compared—filter by an
|
|
2225
|
+
aggregate over the item:
|
|
2226
|
+
|
|
2227
|
+
>>> d = {'items': [{'p': [1, 2]}, {'p': [3]}, {'p': []}]}
|
|
2228
|
+
>>> dotted.get(d, 'items[(p[*];)|sum>2]')
|
|
2229
|
+
[{'p': [1, 2]}, {'p': [3]}]
|
|
2230
|
+
>>> dotted.get(d, 'items[*&(p[*];)|len=0]')
|
|
2231
|
+
({'p': []},)
|
|
2232
|
+
|
|
2233
|
+
A pack is its own path segment: `walk` and `expand` give it back as
|
|
2234
|
+
written, `(a.*;)|sum`. There is nothing under a tuple of matches to write
|
|
2235
|
+
to, so `update` and `remove` through a pack raise `ValueError`, and a pack
|
|
2236
|
+
cannot be negated. `;` is a reserved character; quote a key that contains
|
|
2237
|
+
one, `"a;b"`.
|
|
2238
|
+
|
|
2239
|
+
`(a.*;)[*]` yields the matches again: `[*]` is the inverse of packing.
|
|
2240
|
+
|
|
2150
2241
|
#### Negation (NOT)
|
|
2151
2242
|
|
|
2152
2243
|
Use `!` prefix to exclude keys matching a pattern:
|
|
@@ -2198,7 +2289,8 @@ A guard, filter or transform applies to the whole expression on its left:
|
|
|
2198
2289
|
an access path (on its last segment), a `!` expression (on what it
|
|
2199
2290
|
yields), or a parenthesized group, where it is shorthand for writing it on
|
|
2200
2291
|
every branch: `(a,b)=5` is `(a=5,b=5)` and `(a,b)|int` is `(a|int,b|int)`,
|
|
2201
|
-
which is also how `assemble` writes them.
|
|
2292
|
+
which is also how `assemble` writes them. A [packed](#packing) group
|
|
2293
|
+
yields one value, so `(a,b;)|sum` applies to that tuple once.
|
|
2202
2294
|
|
|
2203
2295
|
>>> d = {'a': 1, 'b': 2, 'c': 3}
|
|
2204
2296
|
>>> dotted.get(d, '!a&b,c') # ((!a)&b), c
|
|
@@ -3068,6 +3160,8 @@ decorator.
|
|
|
3068
3160
|
| `coalesce` | fallbacks... | The first of the value and the fallbacks that is not `None`: `\|coalesce:0` |
|
|
3069
3161
|
| `strip` | `chars`, `raises` | Strip whitespace or specified chars |
|
|
3070
3162
|
| `len` | `default` | Get length. Optional default if not sized: `\|len:0` |
|
|
3163
|
+
| `sum` | `start`, `raises` | Sum a container: `\|sum`, `\|sum:10`; over a pattern, `(a.*;)\|sum` |
|
|
3164
|
+
| `mean` | `raises` | Arithmetic mean of a container: `(a.*;)\|mean` |
|
|
3071
3165
|
| `lowercase` | `raises` | Convert string to lowercase |
|
|
3072
3166
|
| `uppercase` | `raises` | Convert string to uppercase |
|
|
3073
3167
|
| `add` | `rhs`, `raises` | Add value: `\|add:10` |
|
|
@@ -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.49.1"
|
|
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
|
-
|
|
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})
|
|
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.48.1 → dotted_notation-0.49.1}/dotted_notation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|