dotted-notation 0.44.3__tar.gz → 0.44.5__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.44.3 → dotted_notation-0.44.5}/CHANGELOG.md +28 -0
- {dotted_notation-0.44.3/dotted_notation.egg-info → dotted_notation-0.44.5}/PKG-INFO +1 -1
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/__init__.py +2 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/api.py +36 -11
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/engine.py +42 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/matchers.py +9 -2
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/results.py +33 -5
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/utils.py +19 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5/dotted_notation.egg-info}/PKG-INFO +1 -1
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/pyproject.toml +1 -1
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/LICENSE +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/MANIFEST.in +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/README.md +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/__main__.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/access.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/base.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/cli/main.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/containers.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/filters.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/grammar.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/groups.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/predicates.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/recursive.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/sql/core.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/sql/pg.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/transforms.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/utypes.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/wrappers.py +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.44.3 → dotted_notation-0.44.5}/setup.cfg +0 -0
|
@@ -3,6 +3,34 @@
|
|
|
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.44.5]
|
|
7
|
+
|
|
8
|
+
### Performance
|
|
9
|
+
- Transforms no longer disqualify the get() fast path: `a.b|int`
|
|
10
|
+
resolves via the direct-lookup chain and applies transforms on the
|
|
11
|
+
hit (~7x faster than walk). Plain paths are unaffected; `is_simple()`
|
|
12
|
+
semantics unchanged (still requires no transforms).
|
|
13
|
+
|
|
14
|
+
## [0.44.4]
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Module-level escape hatches: `dotted.set_simple_fastpath(False)` disables
|
|
18
|
+
the get() fast path (everything goes through walk()), and
|
|
19
|
+
`dotted.set_parse_cache(size)` resizes the LRU cache behind parse()
|
|
20
|
+
(0 disables, None unbounded). Both return the previous setting.
|
|
21
|
+
|
|
22
|
+
### Performance
|
|
23
|
+
- `get()` fast path for simple paths (#58): a plain chain of literal
|
|
24
|
+
Key/Attr/Slot accesses (`a.b`, `a[0].b`, `a@x`) resolves with direct
|
|
25
|
+
dict/list/attr lookups, skipping the walk() machinery entirely. The
|
|
26
|
+
chain is computed once at parse time and cached on the `Dotted`
|
|
27
|
+
(`simple_chain`); unusual containers (dict subclasses, custom
|
|
28
|
+
mappings) fall back to the full traversal.
|
|
29
|
+
- `Const.value`/`Numeric.value` are now computed once and cached on the
|
|
30
|
+
instance instead of recomputed per property access.
|
|
31
|
+
- `Dotted.__hash__` is cached, making repeated cache lookups keyed on a
|
|
32
|
+
pre-parsed `Dotted` (e.g. `get(obj, parsed)`) much cheaper.
|
|
33
|
+
|
|
6
34
|
## [0.44.3]
|
|
7
35
|
|
|
8
36
|
### Fixed
|
|
@@ -58,6 +58,7 @@ For full documentation including all options and flags:
|
|
|
58
58
|
from .api import \
|
|
59
59
|
parse, is_pattern, is_template, is_reference, is_indeterminate, is_simple, \
|
|
60
60
|
is_inverted, is_mutable, mutable, quote, ANY, AUTO, Attrs, GroupMode, \
|
|
61
|
+
set_simple_fastpath, set_parse_cache, \
|
|
61
62
|
register, transform, \
|
|
62
63
|
assemble, assemble_multi, \
|
|
63
64
|
build, build_multi, \
|
|
@@ -87,6 +88,7 @@ __all__ = [
|
|
|
87
88
|
'is_pattern', 'is_template', 'is_reference',
|
|
88
89
|
'is_indeterminate', 'is_simple',
|
|
89
90
|
'is_inverted', 'is_mutable', 'mutable',
|
|
91
|
+
'set_simple_fastpath', 'set_parse_cache',
|
|
90
92
|
# SQL
|
|
91
93
|
'sqlize', 'Resolver', 'SQLFragment', 'ParamStyle', 'ParamPool', 'TranslationError',
|
|
92
94
|
# Constants
|
|
@@ -69,6 +69,32 @@ class ParseError(Exception):
|
|
|
69
69
|
|
|
70
70
|
_parse_lock = threading.Lock()
|
|
71
71
|
|
|
72
|
+
_SIMPLE_FASTPATH = True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_simple_fastpath(enable=True):
|
|
76
|
+
"""
|
|
77
|
+
Enable/disable the simple-path fast path in get() (escape hatch: when
|
|
78
|
+
off, every lookup goes through the full walk() traversal). On by
|
|
79
|
+
default. Returns the previous setting.
|
|
80
|
+
"""
|
|
81
|
+
global _SIMPLE_FASTPATH
|
|
82
|
+
prev = _SIMPLE_FASTPATH
|
|
83
|
+
_SIMPLE_FASTPATH = bool(enable)
|
|
84
|
+
return prev
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def set_parse_cache(size=CACHE_SIZE):
|
|
88
|
+
"""
|
|
89
|
+
Resize the LRU cache behind parse(). 0 disables caching (every path
|
|
90
|
+
string is re-parsed from scratch); None makes it unbounded. Resizing
|
|
91
|
+
discards currently cached parses. Returns the previous size.
|
|
92
|
+
"""
|
|
93
|
+
global _parse
|
|
94
|
+
prev = _parse.cache_info().maxsize
|
|
95
|
+
_parse = functools.lru_cache(size)(_parse.__wrapped__)
|
|
96
|
+
return prev
|
|
97
|
+
|
|
72
98
|
|
|
73
99
|
@functools.lru_cache(CACHE_SIZE)
|
|
74
100
|
def _parse(ops):
|
|
@@ -234,16 +260,6 @@ def is_indeterminate(path):
|
|
|
234
260
|
return _is_template(parsed) or _is_reference(parsed)
|
|
235
261
|
|
|
236
262
|
|
|
237
|
-
@functools.lru_cache(CACHE_SIZE)
|
|
238
|
-
def _is_simple(ops):
|
|
239
|
-
for op in ops:
|
|
240
|
-
if not isinstance(op, (access.Key, access.Attr, access.Slot)):
|
|
241
|
-
return False
|
|
242
|
-
if not isinstance(op.op, matchers.Const):
|
|
243
|
-
return False
|
|
244
|
-
return True
|
|
245
|
-
|
|
246
|
-
|
|
247
263
|
def is_simple(path):
|
|
248
264
|
"""
|
|
249
265
|
True if the path is a plain chain of access ops (Key, Attr, Slot)
|
|
@@ -273,7 +289,7 @@ def is_simple(path):
|
|
|
273
289
|
parsed = path if isinstance(path, results.Dotted) else parse(path)
|
|
274
290
|
if parsed.transforms:
|
|
275
291
|
return False
|
|
276
|
-
return
|
|
292
|
+
return parsed.simple_chain is not None
|
|
277
293
|
|
|
278
294
|
|
|
279
295
|
def is_inverted(path):
|
|
@@ -433,6 +449,15 @@ def get(obj, path, default=None, pattern_default=(), apply_transforms=True, stri
|
|
|
433
449
|
(7,)
|
|
434
450
|
"""
|
|
435
451
|
ops = parse(path, bindings=bindings, partial=False)
|
|
452
|
+
chain = ops.simple_chain if _SIMPLE_FASTPATH else None
|
|
453
|
+
if chain is not None:
|
|
454
|
+
val = engine.simple_get(chain, obj, strict=strict)
|
|
455
|
+
if val is not engine.SIMPLE_BAIL:
|
|
456
|
+
if val is base.marker:
|
|
457
|
+
return default
|
|
458
|
+
if apply_transforms and ops.transforms:
|
|
459
|
+
return ops.apply(val)
|
|
460
|
+
return val
|
|
436
461
|
vals = engine.iter_until_cut(engine.gets(ops, obj, strict=strict))
|
|
437
462
|
if apply_transforms:
|
|
438
463
|
vals = ( ops.apply(v) for v in vals )
|
|
@@ -48,6 +48,48 @@ def build(ops, node, deepcopy=True, **kwargs):
|
|
|
48
48
|
return built or build_default([cur]+ops)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
SIMPLE_BAIL = object()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def simple_get(chain, node, strict=False):
|
|
55
|
+
"""
|
|
56
|
+
Fast path for simple paths (see Dotted.simple_chain): follow literal
|
|
57
|
+
keys with direct dict/list/attr access, skipping walk() entirely.
|
|
58
|
+
Returns the found value, base.marker when the path misses, or
|
|
59
|
+
SIMPLE_BAIL when a node's type falls outside the fast path — the
|
|
60
|
+
caller then falls back to the full traversal.
|
|
61
|
+
"""
|
|
62
|
+
for kind, key in chain:
|
|
63
|
+
if kind == 'attr':
|
|
64
|
+
node = getattr(node, key, base.marker)
|
|
65
|
+
if node is base.marker:
|
|
66
|
+
return base.marker
|
|
67
|
+
continue
|
|
68
|
+
t = type(node)
|
|
69
|
+
if t is dict:
|
|
70
|
+
# strict: Slot never coerces to dict keys
|
|
71
|
+
if strict and kind == 'slot':
|
|
72
|
+
return base.marker
|
|
73
|
+
node = node.get(key, base.marker)
|
|
74
|
+
if node is base.marker:
|
|
75
|
+
return base.marker
|
|
76
|
+
continue
|
|
77
|
+
if t is list or t is tuple:
|
|
78
|
+
# strict: Key never coerces to sequence indices
|
|
79
|
+
if strict and kind == 'key':
|
|
80
|
+
return base.marker
|
|
81
|
+
if type(key) is not int:
|
|
82
|
+
return base.marker
|
|
83
|
+
try:
|
|
84
|
+
node = node[key]
|
|
85
|
+
except IndexError:
|
|
86
|
+
return base.marker
|
|
87
|
+
continue
|
|
88
|
+
# dict subclasses, custom containers, None, etc: full traversal
|
|
89
|
+
return SIMPLE_BAIL
|
|
90
|
+
return node
|
|
91
|
+
|
|
92
|
+
|
|
51
93
|
def iter_until_cut(gen):
|
|
52
94
|
"""
|
|
53
95
|
Consume a get generator until base.CUT_SENTINEL; yield values, stop on sentinel.
|
|
@@ -10,6 +10,7 @@ import pyparsing as pp
|
|
|
10
10
|
|
|
11
11
|
from . import base
|
|
12
12
|
from .base import MatchOp
|
|
13
|
+
from .utils import lazyprop
|
|
13
14
|
from .utypes import ANY
|
|
14
15
|
|
|
15
16
|
|
|
@@ -19,8 +20,11 @@ _MISSING = object()
|
|
|
19
20
|
class Const(MatchOp):
|
|
20
21
|
_match_from = ('Const',)
|
|
21
22
|
|
|
22
|
-
@
|
|
23
|
+
@lazyprop
|
|
23
24
|
def value(self):
|
|
25
|
+
"""
|
|
26
|
+
The literal value; computed once and cached on the instance.
|
|
27
|
+
"""
|
|
24
28
|
return self.args[0]
|
|
25
29
|
def matches(self, vals):
|
|
26
30
|
return (v for v in vals if self.value == v)
|
|
@@ -32,8 +36,11 @@ class Numeric(Const):
|
|
|
32
36
|
return str(self.args[0]) == str(int(self.args[0]))
|
|
33
37
|
except (ValueError, TypeError):
|
|
34
38
|
return False
|
|
35
|
-
@
|
|
39
|
+
@lazyprop
|
|
36
40
|
def value(self):
|
|
41
|
+
"""
|
|
42
|
+
The numeric value (int when possible); computed once and cached.
|
|
43
|
+
"""
|
|
37
44
|
return int(self.args[0]) if self.is_int() else float(self.args[0])
|
|
38
45
|
def __repr__(self):
|
|
39
46
|
return f'{self.value}'
|
|
@@ -5,7 +5,9 @@ import itertools
|
|
|
5
5
|
|
|
6
6
|
from . import predicates
|
|
7
7
|
from . import utils
|
|
8
|
-
from .access import Invert
|
|
8
|
+
from .access import Attr, Invert, Key, Slot
|
|
9
|
+
from .matchers import Const
|
|
10
|
+
from .utils import lazyprop
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class rdoc(str):
|
|
@@ -35,6 +37,7 @@ class Dotted:
|
|
|
35
37
|
else:
|
|
36
38
|
self.guard = None
|
|
37
39
|
self.guard_op = predicates.EQ
|
|
40
|
+
self._hash = None
|
|
38
41
|
|
|
39
42
|
@property
|
|
40
43
|
def guard_negate(self):
|
|
@@ -72,10 +75,12 @@ class Dotted:
|
|
|
72
75
|
return tuple(sorted((k, Dotted._hashable(v)) for k, v in iterable))
|
|
73
76
|
return obj
|
|
74
77
|
def __hash__(self):
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
if self._hash is None:
|
|
79
|
+
try:
|
|
80
|
+
self._hash = hash((self.ops, self.transforms, self.guard, self.guard_op))
|
|
81
|
+
except TypeError:
|
|
82
|
+
self._hash = hash((self.ops, Dotted._hashable(self.transforms), self.guard, self.guard_op))
|
|
83
|
+
return self._hash
|
|
79
84
|
def __len__(self):
|
|
80
85
|
return len(self.ops)
|
|
81
86
|
def __iter__(self):
|
|
@@ -105,6 +110,29 @@ class Dotted:
|
|
|
105
110
|
def apply(self, val):
|
|
106
111
|
return apply_transforms(val, self.transforms)
|
|
107
112
|
|
|
113
|
+
@lazyprop
|
|
114
|
+
def simple_chain(self):
|
|
115
|
+
"""
|
|
116
|
+
Tuple of (kind, key) pairs when the access chain is simple — a plain
|
|
117
|
+
chain of concrete Key/Attr/Slot accesses with no patterns,
|
|
118
|
+
substitutions, references, guards, or filters — else None. Transforms
|
|
119
|
+
are permitted (they apply after the lookup). Computed once and
|
|
120
|
+
cached; drives the fast path in get() that skips walk().
|
|
121
|
+
kind is 'key', 'attr', or 'slot'.
|
|
122
|
+
"""
|
|
123
|
+
if self.guard is not None:
|
|
124
|
+
return None
|
|
125
|
+
chain = []
|
|
126
|
+
for op in self.ops:
|
|
127
|
+
# exact types: subclasses like SlotSpecial have different semantics
|
|
128
|
+
if type(op) not in (Key, Attr, Slot):
|
|
129
|
+
return None
|
|
130
|
+
if not isinstance(op.op, Const):
|
|
131
|
+
return None
|
|
132
|
+
kind = 'attr' if isinstance(op, Attr) else 'slot' if isinstance(op, Slot) else 'key'
|
|
133
|
+
chain.append((kind, op.op.value))
|
|
134
|
+
return tuple(chain)
|
|
135
|
+
|
|
108
136
|
Dotted.registry.__doc__ = rdoc()
|
|
109
137
|
|
|
110
138
|
|
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
Shared type-checking helpers (duck-typing).
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
class lazyprop:
|
|
6
|
+
"""
|
|
7
|
+
Non-data descriptor: compute once on first access, cache the result
|
|
8
|
+
in the instance __dict__ (which then shadows the descriptor). Like
|
|
9
|
+
functools.cached_property but lock-free and available on 3.6+.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, fn):
|
|
12
|
+
self.fn = fn
|
|
13
|
+
self.name = fn.__name__
|
|
14
|
+
self.__doc__ = fn.__doc__
|
|
15
|
+
|
|
16
|
+
def __get__(self, obj, owner=None):
|
|
17
|
+
if obj is None:
|
|
18
|
+
return self
|
|
19
|
+
val = self.fn(obj)
|
|
20
|
+
obj.__dict__[self.name] = val
|
|
21
|
+
return val
|
|
22
|
+
|
|
23
|
+
|
|
5
24
|
try:
|
|
6
25
|
import dataclasses as _dc
|
|
7
26
|
except ImportError:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dotted_notation"
|
|
7
|
-
version = "0.44.
|
|
7
|
+
version = "0.44.5"
|
|
8
8
|
description = "Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.6"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted_notation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|