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