dotted-notation 0.45.0__tar.gz → 0.46.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.45.0 → dotted_notation-0.46.1}/CHANGELOG.md +47 -0
- dotted_notation-0.46.1/MANIFEST.in +4 -0
- {dotted_notation-0.45.0/dotted_notation.egg-info → dotted_notation-0.46.1}/PKG-INFO +11 -9
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/README.md +3 -2
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/__init__.py +6 -0
- dotted_notation-0.46.1/dotted/access.pxd +57 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/access.py +118 -91
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/api.py +2 -1
- dotted_notation-0.46.1/dotted/base.pxd +70 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/base.py +60 -9
- dotted_notation-0.46.1/dotted/engine.pxd +18 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/engine.py +41 -27
- dotted_notation-0.46.1/dotted/filters.pxd +60 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/grammar.py +151 -12
- dotted_notation-0.46.1/dotted/groups.pxd +34 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/groups.py +37 -173
- dotted_notation-0.46.1/dotted/matchers.pxd +87 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/matchers.py +6 -5
- dotted_notation-0.46.1/dotted/native.py +59 -0
- dotted_notation-0.46.1/dotted/recursive.pxd +24 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/recursive.py +29 -29
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/utils.py +8 -10
- dotted_notation-0.46.1/dotted/wrappers.pxd +27 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/wrappers.py +52 -54
- {dotted_notation-0.45.0 → dotted_notation-0.46.1/dotted_notation.egg-info}/PKG-INFO +11 -9
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted_notation.egg-info/SOURCES.txt +10 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted_notation.egg-info/requires.txt +4 -11
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/pyproject.toml +24 -10
- dotted_notation-0.46.1/setup.py +53 -0
- dotted_notation-0.45.0/MANIFEST.in +0 -2
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/LICENSE +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/__main__.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/cli/main.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/containers.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/filters.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/predicates.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/results.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/sql/core.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/sql/pg.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/transforms.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/utypes.py +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.45.0 → dotted_notation-0.46.1}/setup.cfg +0 -0
|
@@ -3,6 +3,53 @@
|
|
|
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.46.1]
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- Each compiled wheel is built and tested in its own CI job, so a
|
|
10
|
+
release takes minutes rather than the better part of an hour. The CLI
|
|
11
|
+
tests write their input files under pytest's `tmp_path`, which
|
|
12
|
+
Windows accepts.
|
|
13
|
+
|
|
14
|
+
## [0.46.0]
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- The engine compiles to C. Wheels for CPython 3.11-3.14 on Linux
|
|
18
|
+
(glibc and musl, x86_64 and aarch64), macOS (Apple Silicon) and
|
|
19
|
+
Windows carry the compiled engine; every other Python installs the
|
|
20
|
+
same code as pure Python from the universal wheel. `DOTTED_NATIVE=0`
|
|
21
|
+
in the environment runs the Python engine regardless;
|
|
22
|
+
`dotted.native.active()` says which is loaded. Wildcard reads are
|
|
23
|
+
about 2.9x faster than 0.45.1 compiled and writes about 3.5x; pure
|
|
24
|
+
Python is faster too, writes by about a third.
|
|
25
|
+
- For development: `make test` and `make bench` run both engines,
|
|
26
|
+
`test.python`/`test.native` and `bench.python`/`bench.native` one;
|
|
27
|
+
`make test.wheel` runs the suite against a wheel built from the
|
|
28
|
+
source distribution.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- The `all` extra now means all of them: `formats` and `copium`. It was
|
|
32
|
+
an alias for `formats`, deprecated since 0.44.9.
|
|
33
|
+
- A concrete key present in a node is no longer looked for among the
|
|
34
|
+
node's keys to return the node's own key object: it matches as
|
|
35
|
+
written. Concrete writes and `expand`, `pluck` and `unpack` on a
|
|
36
|
+
concrete key cost the same wherever the key sits in a large dict
|
|
37
|
+
(`update` of the 996th key of 1000: 11.5us to 3.3us compiled). The
|
|
38
|
+
one visible difference: a rendered path shows the key as written
|
|
39
|
+
where the node spells it differently but equal, `users.1` for a key
|
|
40
|
+
`1.0`, `RED` for a `StrEnum` member.
|
|
41
|
+
|
|
42
|
+
## [0.45.1]
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- The extras no longer carry environment markers of their own. A marker
|
|
46
|
+
ahead of the extra condition made pypistats list `copium` and `tomli`
|
|
47
|
+
as hard requirements; only `pyparsing` is one. Two consequences:
|
|
48
|
+
`pip install dotted-notation[copium]` now fails where copium cannot be
|
|
49
|
+
installed (below CPython 3.10) instead of skipping it, and the `toml`,
|
|
50
|
+
`formats` and `all` extras install `tomli` on Python 3.11+ as well,
|
|
51
|
+
where it goes unused in favor of the standard library's `tomllib`.
|
|
52
|
+
|
|
6
53
|
## [0.45.0]
|
|
7
54
|
|
|
8
55
|
A performance release. Figures compare against 0.44.11 on a document of
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.46.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
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/freywaid/dotted
|
|
8
8
|
Project-URL: Source, https://github.com/freywaid/dotted
|
|
9
9
|
Project-URL: Changelog, https://github.com/freywaid/dotted/blob/master/CHANGELOG.md
|
|
10
10
|
Keywords: dotted,nested,path,pattern-matching,json,jsonb,sql,traversal
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
13
|
Classifier: Operating System :: OS Independent
|
|
15
14
|
Classifier: Programming Language :: Python :: 3
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.6
|
|
@@ -21,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
25
|
Classifier: Topic :: Utilities
|
|
26
26
|
Requires-Python: >=3.6
|
|
@@ -28,19 +28,20 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
License-File: LICENSE
|
|
29
29
|
Requires-Dist: pyparsing>=3.0
|
|
30
30
|
Provides-Extra: copium
|
|
31
|
-
Requires-Dist: copium>=0.1.0;
|
|
31
|
+
Requires-Dist: copium>=0.1.0; extra == "copium"
|
|
32
32
|
Provides-Extra: yaml
|
|
33
33
|
Requires-Dist: PyYAML>=5.0; extra == "yaml"
|
|
34
34
|
Provides-Extra: toml
|
|
35
|
-
Requires-Dist: tomli>=1.0;
|
|
35
|
+
Requires-Dist: tomli>=1.0; extra == "toml"
|
|
36
36
|
Requires-Dist: tomli_w>=1.0; extra == "toml"
|
|
37
37
|
Provides-Extra: formats
|
|
38
38
|
Requires-Dist: PyYAML>=5.0; extra == "formats"
|
|
39
|
-
Requires-Dist: tomli>=1.0;
|
|
39
|
+
Requires-Dist: tomli>=1.0; extra == "formats"
|
|
40
40
|
Requires-Dist: tomli_w>=1.0; extra == "formats"
|
|
41
41
|
Provides-Extra: all
|
|
42
|
+
Requires-Dist: copium>=0.1.0; extra == "all"
|
|
42
43
|
Requires-Dist: PyYAML>=5.0; extra == "all"
|
|
43
|
-
Requires-Dist: tomli>=1.0;
|
|
44
|
+
Requires-Dist: tomli>=1.0; extra == "all"
|
|
44
45
|
Requires-Dist: tomli_w>=1.0; extra == "all"
|
|
45
46
|
Dynamic: license-file
|
|
46
47
|
|
|
@@ -73,8 +74,9 @@ Or pick only what you need:
|
|
|
73
74
|
|
|
74
75
|
pip install dotted-notation[yaml,toml]
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
Or everything, formats and copium (below) alike:
|
|
78
|
+
|
|
79
|
+
pip install dotted-notation[all]
|
|
78
80
|
|
|
79
81
|
For faster `mutable=False` updates and removes, install the
|
|
80
82
|
[copium](https://github.com/Bobronium/copium) extra, a C implementation of
|
|
@@ -27,8 +27,9 @@ Or pick only what you need:
|
|
|
27
27
|
|
|
28
28
|
pip install dotted-notation[yaml,toml]
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
Or everything, formats and copium (below) alike:
|
|
31
|
+
|
|
32
|
+
pip install dotted-notation[all]
|
|
32
33
|
|
|
33
34
|
For faster `mutable=False` updates and removes, install the
|
|
34
35
|
[copium](https://github.com/Bobronium/copium) extra, a C implementation of
|
|
@@ -55,6 +55,12 @@ For full documentation including all options and flags:
|
|
|
55
55
|
$ pydoc dotted.api
|
|
56
56
|
or see README.md
|
|
57
57
|
"""
|
|
58
|
+
from . import native
|
|
59
|
+
|
|
60
|
+
# before anything else of dotted's is imported: see dotted.native
|
|
61
|
+
if not native.wanted():
|
|
62
|
+
native.use_source()
|
|
63
|
+
|
|
58
64
|
from .api import \
|
|
59
65
|
parse, compile, is_pattern, is_template, is_reference, is_indeterminate, is_simple, \
|
|
60
66
|
is_inverted, is_mutable, mutable, quote, ANY, AUTO, Attrs, GroupMode, \
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Type declarations for the compiler; see base.pxd.
|
|
2
|
+
from . cimport base
|
|
3
|
+
from . cimport matchers
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
cdef class BaseOp(base.TraversalOp):
|
|
7
|
+
cdef public tuple filters
|
|
8
|
+
|
|
9
|
+
cpdef filtered(self, items)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
cdef class SimpleOp(BaseOp):
|
|
13
|
+
cpdef items(self, node, settings=*, filtered=*)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
cdef class Empty(SimpleOp):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
cdef class AccessOp(SimpleOp):
|
|
21
|
+
cdef public object op
|
|
22
|
+
|
|
23
|
+
cpdef quote(self)
|
|
24
|
+
cpdef is_pattern(self)
|
|
25
|
+
cpdef is_template(self)
|
|
26
|
+
cpdef is_reference(self)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
cdef class Key(AccessOp):
|
|
30
|
+
cpdef _const_items(self, node)
|
|
31
|
+
cpdef operator(self, top=*)
|
|
32
|
+
cpdef default(self)
|
|
33
|
+
cpdef match(self, op, specials=*)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
cdef class Attr(Key):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
cdef class Slot(Key):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
cdef class SlotSpecial(Slot):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
cdef class SliceFilter(BaseOp):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
cdef class Slice(SimpleOp):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
cdef class Invert(SimpleOp):
|
|
57
|
+
pass
|
|
@@ -35,51 +35,45 @@ class BaseOp(base.TraversalOp):
|
|
|
35
35
|
items = f.filtered(items)
|
|
36
36
|
return items
|
|
37
37
|
|
|
38
|
-
def keys(self, node,
|
|
39
|
-
return (k for k, _ in self.items(node,
|
|
38
|
+
def keys(self, node, settings=base.SETTINGS):
|
|
39
|
+
return (k for k, _ in self.items(node, settings=settings))
|
|
40
40
|
|
|
41
|
-
def values(self, node,
|
|
42
|
-
return (v for _, v in self.items(node,
|
|
41
|
+
def values(self, node, settings=base.SETTINGS):
|
|
42
|
+
return (v for _, v in self.items(node, settings=settings))
|
|
43
43
|
|
|
44
|
-
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False,
|
|
44
|
+
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, settings=base.SETTINGS):
|
|
45
45
|
if not ops:
|
|
46
46
|
if nop:
|
|
47
47
|
return node
|
|
48
|
-
if
|
|
48
|
+
if settings.strict and not any(True for _ in self.items(node, settings=settings)):
|
|
49
49
|
return node
|
|
50
50
|
return self.upsert(node, val)
|
|
51
|
-
items = self.update_items(node,
|
|
51
|
+
items = self.update_items(node, settings=settings)
|
|
52
52
|
if items is None:
|
|
53
53
|
if not has_defaults:
|
|
54
54
|
if nop or isinstance(ops[0], wrappers.NopWrap):
|
|
55
55
|
return node
|
|
56
|
-
built = engine.updates(ops, engine.build_default(ops), val, True, _path, nop,
|
|
56
|
+
built = engine.updates(ops, engine.build_default(ops), val, True, _path, nop, settings=settings)
|
|
57
57
|
return self.upsert(node, built)
|
|
58
|
-
items = self.items(node,
|
|
59
|
-
|
|
60
|
-
if kwargs.get('_parents') is not None:
|
|
61
|
-
inner_kwargs = dict(kwargs)
|
|
62
|
-
inner_kwargs['_parents'] = (node,) + kwargs['_parents']
|
|
58
|
+
items = self.items(node, settings=settings)
|
|
59
|
+
inner = settings.below(node)
|
|
63
60
|
pass_nop = nop and not nop_from_unwrap
|
|
64
61
|
for k, v in items:
|
|
65
62
|
if v is None:
|
|
66
63
|
v = engine.build_default(ops)
|
|
67
|
-
node = self.update(node, k, engine.updates(ops, v, val, has_defaults, _path + [(self, k)], pass_nop,
|
|
64
|
+
node = self.update(node, k, engine.updates(ops, v, val, has_defaults, _path + [(self, k)], pass_nop, settings=inner))
|
|
68
65
|
return node
|
|
69
66
|
|
|
70
|
-
def do_remove(self, ops, node, val, nop,
|
|
67
|
+
def do_remove(self, ops, node, val, nop, settings=base.SETTINGS):
|
|
71
68
|
if not ops:
|
|
72
69
|
if nop:
|
|
73
70
|
return node
|
|
74
|
-
if
|
|
71
|
+
if settings.strict and not any(True for _ in self.items(node, settings=settings)):
|
|
75
72
|
return node
|
|
76
73
|
return self.remove(node, val)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
inner_kwargs['_parents'] = (node,) + kwargs['_parents']
|
|
81
|
-
for k, v in self.items(node, **kwargs):
|
|
82
|
-
node = self.update(node, k, engine.removes(ops, v, val, nop=False, **inner_kwargs))
|
|
74
|
+
inner = settings.below(node)
|
|
75
|
+
for k, v in self.items(node, settings=settings):
|
|
76
|
+
node = self.update(node, k, engine.removes(ops, v, val, nop=False, settings=inner))
|
|
83
77
|
return node
|
|
84
78
|
|
|
85
79
|
|
|
@@ -89,6 +83,12 @@ class SimpleOp(BaseOp):
|
|
|
89
83
|
Base for ops with items()/concrete() that share the standard walk pattern.
|
|
90
84
|
"""
|
|
91
85
|
|
|
86
|
+
def items(self, node, settings=base.SETTINGS, filtered=True):
|
|
87
|
+
"""
|
|
88
|
+
(key, value) for each match of this op in node.
|
|
89
|
+
"""
|
|
90
|
+
raise NotImplementedError
|
|
91
|
+
|
|
92
92
|
def push_children(self, stack, frame, paths):
|
|
93
93
|
"""
|
|
94
94
|
Push matching children onto the traversal stack.
|
|
@@ -97,18 +97,13 @@ class SimpleOp(BaseOp):
|
|
|
97
97
|
NOTE: We tried skipping list()+reversed() for concrete (non-pattern)
|
|
98
98
|
keys since they match at most one item. Benchmarked as neutral —
|
|
99
99
|
the is_pattern() check costs roughly what the single-item reversal
|
|
100
|
-
saves.
|
|
101
|
-
(always passing a dict instead of None). Also neutral. Keeping
|
|
102
|
-
the simple uniform path for clarity.
|
|
100
|
+
saves. Keeping the simple uniform path for clarity.
|
|
103
101
|
"""
|
|
104
|
-
children = list(self.items(frame.node,
|
|
105
|
-
|
|
106
|
-
if kw and kw.get('_parents') is not None:
|
|
107
|
-
kw = dict(kw)
|
|
108
|
-
kw['_parents'] = (frame.node,) + kw['_parents']
|
|
102
|
+
children = list(self.items(frame.node, settings=frame.settings))
|
|
103
|
+
below = frame.settings.below(frame.node)
|
|
109
104
|
for k, v in reversed(children):
|
|
110
105
|
cp = frame.prefix + (self.concrete(k),) if paths else frame.prefix
|
|
111
|
-
stack.push(base.Frame(frame.ops, v, cp,
|
|
106
|
+
stack.push(base.Frame(frame.ops, v, cp, settings=below))
|
|
112
107
|
return ()
|
|
113
108
|
|
|
114
109
|
|
|
@@ -143,20 +138,24 @@ class Empty(SimpleOp):
|
|
|
143
138
|
def operator(self, top=False):
|
|
144
139
|
return self.__repr__()
|
|
145
140
|
|
|
146
|
-
def items(self, node,
|
|
141
|
+
def items(self, node, settings=base.SETTINGS, filtered=True):
|
|
147
142
|
"""
|
|
148
|
-
|
|
143
|
+
The root as a single item with empty key.
|
|
149
144
|
"""
|
|
145
|
+
return self._root_items(node)
|
|
146
|
+
|
|
147
|
+
def _root_items(self, node):
|
|
150
148
|
for v in self.filtered((node,)):
|
|
151
149
|
yield ('', v)
|
|
152
150
|
|
|
153
|
-
def keys(self, node,
|
|
151
|
+
def keys(self, node, settings=base.SETTINGS):
|
|
154
152
|
"""
|
|
155
153
|
Yield empty string as the 'key' for root.
|
|
156
154
|
"""
|
|
157
|
-
|
|
155
|
+
items = self.items(node, settings=settings)
|
|
156
|
+
return (k for k, _ in items)
|
|
158
157
|
|
|
159
|
-
def values(self, node,
|
|
158
|
+
def values(self, node, settings=base.SETTINGS):
|
|
160
159
|
return self.filtered((node,))
|
|
161
160
|
|
|
162
161
|
def default(self):
|
|
@@ -254,14 +253,14 @@ class AccessOp(SimpleOp):
|
|
|
254
253
|
return self
|
|
255
254
|
return self.__class__(new_op)
|
|
256
255
|
|
|
257
|
-
def _resolved(self, node=None,
|
|
256
|
+
def _resolved(self, node=None, settings=base.SETTINGS):
|
|
258
257
|
"""
|
|
259
258
|
Resolve a Reference against the appropriate target. Returns a
|
|
260
259
|
tuple of concrete ops (empty if the reference path is not found).
|
|
261
260
|
Callers must guard with is_reference() before calling.
|
|
262
261
|
"""
|
|
263
|
-
root =
|
|
264
|
-
parents =
|
|
262
|
+
root = settings.root
|
|
263
|
+
parents = settings.parents or ()
|
|
265
264
|
try:
|
|
266
265
|
val = self.op.resolve_ref(root, node=node, parents=parents)
|
|
267
266
|
except KeyError:
|
|
@@ -270,39 +269,40 @@ class AccessOp(SimpleOp):
|
|
|
270
269
|
return tuple(self.__class__(matchers.Const(v)) for v in val)
|
|
271
270
|
return (self.__class__(matchers.Const(val)),)
|
|
272
271
|
|
|
273
|
-
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False,
|
|
272
|
+
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, settings=base.SETTINGS):
|
|
274
273
|
"""
|
|
275
274
|
Resolve references before updating.
|
|
276
275
|
"""
|
|
277
276
|
if self.is_reference():
|
|
278
|
-
for r in self._resolved(node=node,
|
|
279
|
-
node = r.do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap,
|
|
277
|
+
for r in self._resolved(node=node, settings=settings):
|
|
278
|
+
node = r.do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap, settings=settings)
|
|
280
279
|
return node
|
|
281
|
-
return super().do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap,
|
|
280
|
+
return super().do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap, settings=settings)
|
|
282
281
|
|
|
283
|
-
def do_remove(self, ops, node, val, nop,
|
|
282
|
+
def do_remove(self, ops, node, val, nop, settings=base.SETTINGS):
|
|
284
283
|
"""
|
|
285
284
|
Resolve references before removing.
|
|
286
285
|
"""
|
|
287
286
|
if self.is_reference():
|
|
288
|
-
for r in self._resolved(node=node,
|
|
289
|
-
node = r.do_remove(ops, node, val, nop,
|
|
287
|
+
for r in self._resolved(node=node, settings=settings):
|
|
288
|
+
node = r.do_remove(ops, node, val, nop, settings=settings)
|
|
290
289
|
return node
|
|
291
|
-
return super().do_remove(ops, node, val, nop,
|
|
290
|
+
return super().do_remove(ops, node, val, nop, settings=settings)
|
|
292
291
|
|
|
293
292
|
def is_empty(self, node):
|
|
294
293
|
return not any(True for _ in self.keys(node))
|
|
295
294
|
|
|
296
|
-
def update_items(self, node,
|
|
295
|
+
def update_items(self, node, settings=base.SETTINGS):
|
|
297
296
|
"""
|
|
298
297
|
One lookup, not two: is_empty() above asks whether items() yields
|
|
299
298
|
anything, so take the first item and keep the rest. Only when the
|
|
300
299
|
two would see the same items: is_empty() ignores strict, and a
|
|
301
300
|
subclass may define emptiness differently.
|
|
302
301
|
"""
|
|
303
|
-
if
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
if settings.strict or type(self).is_empty is not AccessOp.is_empty:
|
|
303
|
+
# explicit super: the compiler gives a cpdef method no __class__ cell
|
|
304
|
+
return super(AccessOp, self).update_items(node, settings=settings)
|
|
305
|
+
items = iter(self.items(node, settings=settings))
|
|
306
306
|
first = next(items, base.marker)
|
|
307
307
|
if first is base.marker:
|
|
308
308
|
return None
|
|
@@ -367,17 +367,39 @@ class Key(AccessOp):
|
|
|
367
367
|
|
|
368
368
|
return _items()
|
|
369
369
|
|
|
370
|
-
def
|
|
370
|
+
def _reference_items(self, node, filtered, settings):
|
|
371
|
+
"""
|
|
372
|
+
The items of each op this reference resolves to, chained.
|
|
373
|
+
"""
|
|
374
|
+
return itertools.chain.from_iterable(
|
|
375
|
+
r.items(node, filtered=filtered, settings=settings)
|
|
376
|
+
for r in self._resolved(node=node, settings=settings))
|
|
377
|
+
|
|
378
|
+
def _const_items(self, node):
|
|
379
|
+
"""
|
|
380
|
+
The items of a constant key: what _items(node, match_keys(node))
|
|
381
|
+
yields, looked up now instead of through a generator, since there is
|
|
382
|
+
at most one.
|
|
383
|
+
"""
|
|
384
|
+
pairs = []
|
|
385
|
+
for k in self.op.match_keys(node):
|
|
386
|
+
try:
|
|
387
|
+
pairs.append((k, node[k]))
|
|
388
|
+
except (TypeError, KeyError, IndexError):
|
|
389
|
+
continue
|
|
390
|
+
return iter(pairs)
|
|
391
|
+
|
|
392
|
+
def items(self, node, settings=base.SETTINGS, filtered=True):
|
|
371
393
|
if self.is_reference():
|
|
372
|
-
return
|
|
373
|
-
r.items(node, filtered=filtered, **kwargs)
|
|
374
|
-
for r in self._resolved(node=node, **kwargs))
|
|
394
|
+
return self._reference_items(node, filtered, settings)
|
|
375
395
|
# Dict-like: use key matching
|
|
376
396
|
if hasattr(node, 'keys'):
|
|
397
|
+
if filtered and not self.filters and isinstance(self.op, matchers.Const):
|
|
398
|
+
return self._const_items(node)
|
|
377
399
|
keys = self.op.match_keys(node) if filtered else node.keys()
|
|
378
400
|
return self._items(node, keys, filtered)
|
|
379
401
|
# In strict mode, numeric keys never coerce to list indices
|
|
380
|
-
if
|
|
402
|
+
if settings.strict:
|
|
381
403
|
return ()
|
|
382
404
|
# Key only handles lists with concrete numeric keys
|
|
383
405
|
if not hasattr(node, '__getitem__'):
|
|
@@ -415,7 +437,8 @@ class Key(AccessOp):
|
|
|
415
437
|
if not self.op.matchable(op.op, specials):
|
|
416
438
|
return None
|
|
417
439
|
|
|
418
|
-
|
|
440
|
+
# explicit super: the compiler gives a cpdef method no __class__ cell
|
|
441
|
+
results = super(Key, self).match(op)
|
|
419
442
|
if results is None:
|
|
420
443
|
return None
|
|
421
444
|
|
|
@@ -460,8 +483,8 @@ class Key(AccessOp):
|
|
|
460
483
|
except TypeError:
|
|
461
484
|
pass
|
|
462
485
|
return type(node)((k, v) for k, v in node.items() if k != key)
|
|
463
|
-
def remove(self, node, val,
|
|
464
|
-
to_remove = [k for k, v in self.items(node,
|
|
486
|
+
def remove(self, node, val, settings=base.SETTINGS):
|
|
487
|
+
to_remove = [k for k, v in self.items(node, settings=settings) if val is base.ANY or v == val]
|
|
465
488
|
for k in to_remove:
|
|
466
489
|
node = self.pop(node, k)
|
|
467
490
|
return node
|
|
@@ -556,11 +579,9 @@ class Attr(Key):
|
|
|
556
579
|
return
|
|
557
580
|
yield (key, v)
|
|
558
581
|
|
|
559
|
-
def items(self, node, filtered=True
|
|
582
|
+
def items(self, node, settings=base.SETTINGS, filtered=True):
|
|
560
583
|
if self.is_reference():
|
|
561
|
-
return
|
|
562
|
-
r.items(node, filtered=filtered, **kwargs)
|
|
563
|
-
for r in self._resolved(node=node, **kwargs))
|
|
584
|
+
return self._reference_items(node, filtered, settings)
|
|
564
585
|
all_keys = self._all_keys(node)
|
|
565
586
|
keys = self.op.matches(all_keys) if filtered else all_keys
|
|
566
587
|
result = self._items(node, keys, filtered)
|
|
@@ -618,8 +639,8 @@ class Attr(Key):
|
|
|
618
639
|
pass
|
|
619
640
|
return node
|
|
620
641
|
|
|
621
|
-
def remove(self, node, val,
|
|
622
|
-
to_remove = [k for k, v in self.items(node,
|
|
642
|
+
def remove(self, node, val, settings=base.SETTINGS):
|
|
643
|
+
to_remove = [k for k, v in self.items(node, settings=settings) if val is base.ANY or v == val]
|
|
623
644
|
for k in to_remove:
|
|
624
645
|
self.pop(node, k)
|
|
625
646
|
return node
|
|
@@ -645,15 +666,14 @@ class Slot(Key):
|
|
|
645
666
|
return '[]'
|
|
646
667
|
return '[' + self.op.quote() + ']'
|
|
647
668
|
|
|
648
|
-
def items(self, node, filtered=True
|
|
669
|
+
def items(self, node, settings=base.SETTINGS, filtered=True):
|
|
649
670
|
if self.is_reference():
|
|
650
|
-
return
|
|
651
|
-
r.items(node, filtered=filtered, **kwargs)
|
|
652
|
-
for r in self._resolved(node=node, **kwargs))
|
|
671
|
+
return self._reference_items(node, filtered, settings)
|
|
653
672
|
if hasattr(node, 'keys'):
|
|
654
|
-
if
|
|
673
|
+
if settings.strict:
|
|
655
674
|
return ()
|
|
656
|
-
|
|
675
|
+
# explicit super: the compiler gives a cpdef method no __class__ cell
|
|
676
|
+
return super(Slot, self).items(node, filtered=filtered, settings=settings)
|
|
657
677
|
|
|
658
678
|
if not hasattr(node, '__getitem__'):
|
|
659
679
|
return ()
|
|
@@ -661,7 +681,7 @@ class Slot(Key):
|
|
|
661
681
|
if not filtered:
|
|
662
682
|
keys = range(len(node))
|
|
663
683
|
elif self.is_pattern():
|
|
664
|
-
keys = self.op.matches(
|
|
684
|
+
keys = self.op.matches(utils.indices(node))
|
|
665
685
|
else:
|
|
666
686
|
keys = (self.op.value,)
|
|
667
687
|
|
|
@@ -670,7 +690,8 @@ class Slot(Key):
|
|
|
670
690
|
def default(self):
|
|
671
691
|
if isinstance(self.op, matchers.Numeric) and self.op.is_int():
|
|
672
692
|
return []
|
|
673
|
-
|
|
693
|
+
# explicit super: the compiler gives a cpdef method no __class__ cell
|
|
694
|
+
return super(Slot, self).default()
|
|
674
695
|
|
|
675
696
|
def update(self, node, key, val):
|
|
676
697
|
if hasattr(node, 'keys'):
|
|
@@ -777,7 +798,10 @@ class SlotSpecial(Slot):
|
|
|
777
798
|
def default(self):
|
|
778
799
|
return []
|
|
779
800
|
|
|
780
|
-
def items(self, node,
|
|
801
|
+
def items(self, node, settings=base.SETTINGS, filtered=True):
|
|
802
|
+
return self._last_item(node)
|
|
803
|
+
|
|
804
|
+
def _last_item(self, node):
|
|
781
805
|
try:
|
|
782
806
|
yield -1, node[-1]
|
|
783
807
|
except (TypeError, IndexError):
|
|
@@ -869,7 +893,7 @@ class SliceFilter(BaseOp):
|
|
|
869
893
|
return None
|
|
870
894
|
return super().match(op)
|
|
871
895
|
|
|
872
|
-
def items(self, node,
|
|
896
|
+
def items(self, node, settings=base.SETTINGS):
|
|
873
897
|
for idx, v in enumerate(node):
|
|
874
898
|
if any(True for _ in self.filtered((v,))):
|
|
875
899
|
yield (idx, v)
|
|
@@ -880,8 +904,8 @@ class SliceFilter(BaseOp):
|
|
|
880
904
|
def update(self, node, key, val):
|
|
881
905
|
raise RuntimeError('Updates not supported for slice filtering')
|
|
882
906
|
|
|
883
|
-
def remove(self, node, val,
|
|
884
|
-
removes = [idx for idx, _ in self.items(node,
|
|
907
|
+
def remove(self, node, val, settings=base.SETTINGS):
|
|
908
|
+
removes = [idx for idx, _ in self.items(node, settings=settings)]
|
|
885
909
|
|
|
886
910
|
if not removes:
|
|
887
911
|
return node
|
|
@@ -918,7 +942,7 @@ class SliceFilter(BaseOp):
|
|
|
918
942
|
"""
|
|
919
943
|
filtered = type(frame.node)(self.filtered(frame.node))
|
|
920
944
|
cp = frame.prefix + (Slice.concrete(slice(None)),) if paths else frame.prefix
|
|
921
|
-
stack.push(base.Frame(frame.ops, filtered, cp,
|
|
945
|
+
stack.push(base.Frame(frame.ops, filtered, cp, settings=frame.settings))
|
|
922
946
|
return ()
|
|
923
947
|
|
|
924
948
|
|
|
@@ -980,16 +1004,19 @@ class Slice(SimpleOp):
|
|
|
980
1004
|
return 1 << 64
|
|
981
1005
|
return max(0, int((stop - start) / step))
|
|
982
1006
|
|
|
983
|
-
def keys(self, node,
|
|
1007
|
+
def keys(self, node, settings=base.SETTINGS):
|
|
984
1008
|
return (self.slice(node),)
|
|
985
|
-
def items(self, node,
|
|
986
|
-
|
|
1009
|
+
def items(self, node, settings=base.SETTINGS, filtered=True):
|
|
1010
|
+
return self._slice_items(node, settings)
|
|
1011
|
+
def _slice_items(self, node, settings):
|
|
1012
|
+
for k in self.keys(node, settings=settings):
|
|
987
1013
|
try:
|
|
988
1014
|
yield (k, node[k])
|
|
989
1015
|
except (TypeError, KeyError, IndexError):
|
|
990
1016
|
pass
|
|
991
|
-
def values(self, node,
|
|
992
|
-
|
|
1017
|
+
def values(self, node, settings=base.SETTINGS):
|
|
1018
|
+
items = self.items(node, settings=settings)
|
|
1019
|
+
return (v for _, v in items)
|
|
993
1020
|
def is_empty(self, node):
|
|
994
1021
|
return not node[self.slice(node)]
|
|
995
1022
|
def default(self):
|
|
@@ -1067,19 +1094,19 @@ class Invert(SimpleOp):
|
|
|
1067
1094
|
return '-'
|
|
1068
1095
|
def match(self, op, specials=False):
|
|
1069
1096
|
return base.MatchResult('-') if isinstance(op, Invert) else None
|
|
1070
|
-
def items(self, node,
|
|
1071
|
-
|
|
1072
|
-
def keys(self, node,
|
|
1097
|
+
def items(self, node, settings=base.SETTINGS, filtered=True):
|
|
1098
|
+
return iter((('-', node),))
|
|
1099
|
+
def keys(self, node, settings=base.SETTINGS):
|
|
1073
1100
|
yield '-'
|
|
1074
|
-
def values(self, node,
|
|
1101
|
+
def values(self, node, settings=base.SETTINGS):
|
|
1075
1102
|
yield node
|
|
1076
1103
|
|
|
1077
|
-
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False,
|
|
1078
|
-
return engine.removes(ops, node, val,
|
|
1104
|
+
def do_update(self, ops, node, val, has_defaults, _path, nop, nop_from_unwrap=False, settings=base.SETTINGS):
|
|
1105
|
+
return engine.removes(ops, node, val, settings=settings)
|
|
1079
1106
|
|
|
1080
|
-
def do_remove(self, ops, node, val, nop,
|
|
1107
|
+
def do_remove(self, ops, node, val, nop, settings=base.SETTINGS):
|
|
1081
1108
|
assert val is not base.ANY, 'Value required'
|
|
1082
|
-
return engine.updates(ops, node, val,
|
|
1109
|
+
return engine.updates(ops, node, val, settings=settings)
|
|
1083
1110
|
|
|
1084
1111
|
|
|
1085
1112
|
# engine and wrappers both import this module, so they can only be bound
|
|
@@ -372,6 +372,7 @@ def mutable(obj, path, strict=False, bindings=None):
|
|
|
372
372
|
return False
|
|
373
373
|
|
|
374
374
|
# Walk the path - if we find any mutable container, mutation will occur
|
|
375
|
+
settings = base.Settings(obj, strict)
|
|
375
376
|
current = obj
|
|
376
377
|
for op in ops:
|
|
377
378
|
if isinstance(op, access.Invert):
|
|
@@ -382,7 +383,7 @@ def mutable(obj, path, strict=False, bindings=None):
|
|
|
382
383
|
|
|
383
384
|
# Traverse to next level (consume only first value)
|
|
384
385
|
_marker = object()
|
|
385
|
-
first = next(op.values(current,
|
|
386
|
+
first = next(op.values(current, settings=settings), _marker)
|
|
386
387
|
if first is _marker:
|
|
387
388
|
# Path doesn't exist - would be created, but parent is immutable
|
|
388
389
|
return False
|