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.
Files changed (49) hide show
  1. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/CHANGELOG.md +47 -0
  2. dotted_notation-0.46.1/MANIFEST.in +4 -0
  3. {dotted_notation-0.45.0/dotted_notation.egg-info → dotted_notation-0.46.1}/PKG-INFO +11 -9
  4. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/README.md +3 -2
  5. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/__init__.py +6 -0
  6. dotted_notation-0.46.1/dotted/access.pxd +57 -0
  7. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/access.py +118 -91
  8. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/api.py +2 -1
  9. dotted_notation-0.46.1/dotted/base.pxd +70 -0
  10. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/base.py +60 -9
  11. dotted_notation-0.46.1/dotted/engine.pxd +18 -0
  12. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/engine.py +41 -27
  13. dotted_notation-0.46.1/dotted/filters.pxd +60 -0
  14. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/grammar.py +151 -12
  15. dotted_notation-0.46.1/dotted/groups.pxd +34 -0
  16. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/groups.py +37 -173
  17. dotted_notation-0.46.1/dotted/matchers.pxd +87 -0
  18. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/matchers.py +6 -5
  19. dotted_notation-0.46.1/dotted/native.py +59 -0
  20. dotted_notation-0.46.1/dotted/recursive.pxd +24 -0
  21. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/recursive.py +29 -29
  22. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/utils.py +8 -10
  23. dotted_notation-0.46.1/dotted/wrappers.pxd +27 -0
  24. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/wrappers.py +52 -54
  25. {dotted_notation-0.45.0 → dotted_notation-0.46.1/dotted_notation.egg-info}/PKG-INFO +11 -9
  26. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted_notation.egg-info/SOURCES.txt +10 -0
  27. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted_notation.egg-info/requires.txt +4 -11
  28. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/pyproject.toml +24 -10
  29. dotted_notation-0.46.1/setup.py +53 -0
  30. dotted_notation-0.45.0/MANIFEST.in +0 -2
  31. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/LICENSE +0 -0
  32. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/__main__.py +0 -0
  33. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/cli/__init__.py +0 -0
  34. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/cli/_compat.py +0 -0
  35. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/cli/formats.py +0 -0
  36. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/cli/main.py +0 -0
  37. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/containers.py +0 -0
  38. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/filters.py +0 -0
  39. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/predicates.py +0 -0
  40. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/results.py +0 -0
  41. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/sql/__init__.py +0 -0
  42. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/sql/core.py +0 -0
  43. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/sql/pg.py +0 -0
  44. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/transforms.py +0 -0
  45. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted/utypes.py +0 -0
  46. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted_notation.egg-info/dependency_links.txt +0 -0
  47. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted_notation.egg-info/entry_points.txt +0 -0
  48. {dotted_notation-0.45.0 → dotted_notation-0.46.1}/dotted_notation.egg-info/top_level.txt +0 -0
  49. {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
@@ -0,0 +1,4 @@
1
+ include CHANGELOG.md
2
+ recursive-include dotted *.pxd
3
+ recursive-exclude dotted *.c *.so *.pyd
4
+ prune tests
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.45.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; (python_version >= "3.10" and platform_python_implementation == "CPython") and extra == "copium"
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; python_version < "3.11" and extra == "toml"
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; python_version < "3.11" and extra == "formats"
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; python_version < "3.11" and extra == "all"
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
- > **Deprecated:** the `all` extra is an alias for `formats` and will be removed
77
- > in a future release. Use `formats` instead.
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
- > **Deprecated:** the `all` extra is an alias for `formats` and will be removed
31
- > in a future release. Use `formats` instead.
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, **kwargs):
39
- return (k for k, _ in self.items(node, **kwargs))
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, **kwargs):
42
- return (v for _, v in self.items(node, **kwargs))
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, **kwargs):
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 kwargs.get('strict') and not any(True for _ in self.items(node, **kwargs)):
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, **kwargs)
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, **kwargs)
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, **kwargs)
59
- inner_kwargs = kwargs
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, **inner_kwargs))
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, **kwargs):
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 kwargs.get('strict') and not any(True for _ in self.items(node, **kwargs)):
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
- inner_kwargs = kwargs
78
- if kwargs.get('_parents') is not None:
79
- inner_kwargs = dict(kwargs)
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. We also tried removing the ``or {}`` guard on kwargs
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, **(frame.kwargs or {})))
105
- kw = frame.kwargs
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, kwargs=kw))
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, **kwargs):
141
+ def items(self, node, settings=base.SETTINGS, filtered=True):
147
142
  """
148
- Yield the root as a single item with empty key.
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, **kwargs):
151
+ def keys(self, node, settings=base.SETTINGS):
154
152
  """
155
153
  Yield empty string as the 'key' for root.
156
154
  """
157
- return (k for k, _ in self.items(node, **kwargs))
155
+ items = self.items(node, settings=settings)
156
+ return (k for k, _ in items)
158
157
 
159
- def values(self, node, **kwargs):
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, **kwargs):
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 = (kwargs or {}).get('_root')
264
- parents = (kwargs or {}).get('_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, **kwargs):
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, **kwargs):
279
- node = r.do_update(ops, node, val, has_defaults, _path, nop, nop_from_unwrap=nop_from_unwrap, **kwargs)
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, **kwargs)
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, **kwargs):
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, **kwargs):
289
- node = r.do_remove(ops, node, val, nop, **kwargs)
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, **kwargs)
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, **kwargs):
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 kwargs.get('strict') or type(self).is_empty is not AccessOp.is_empty:
304
- return super().update_items(node, **kwargs)
305
- items = iter(self.items(node, **kwargs))
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 items(self, node, filtered=True, **kwargs):
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 itertools.chain.from_iterable(
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 kwargs.get('strict'):
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
- results = super().match(op)
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, **kwargs):
464
- to_remove = [k for k, v in self.items(node, **kwargs) if val is base.ANY or v == val]
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, **kwargs):
582
+ def items(self, node, settings=base.SETTINGS, filtered=True):
560
583
  if self.is_reference():
561
- return itertools.chain.from_iterable(
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, **kwargs):
622
- to_remove = [k for k, v in self.items(node, **kwargs) if val is base.ANY or v == val]
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, **kwargs):
669
+ def items(self, node, settings=base.SETTINGS, filtered=True):
649
670
  if self.is_reference():
650
- return itertools.chain.from_iterable(
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 kwargs.get('strict'):
673
+ if settings.strict:
655
674
  return ()
656
- return super().items(node, filtered, **kwargs)
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(idx for idx, _ in enumerate(node))
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
- return super().default()
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, **kwargs):
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, **kwargs):
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, **kwargs):
884
- removes = [idx for idx, _ in self.items(node, **kwargs)]
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, kwargs=frame.kwargs))
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, **kwargs):
1007
+ def keys(self, node, settings=base.SETTINGS):
984
1008
  return (self.slice(node),)
985
- def items(self, node, **kwargs):
986
- for k in self.keys(node, **kwargs):
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, **kwargs):
992
- return (v for _, v in self.items(node, **kwargs))
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, **kwargs):
1071
- yield ('-', node)
1072
- def keys(self, node, **kwargs):
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, **kwargs):
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, **kwargs):
1078
- return engine.removes(ops, node, val, **kwargs)
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, **kwargs):
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, **kwargs)
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, strict=strict), _marker)
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