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.
Files changed (38) hide show
  1. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/CHANGELOG.md +28 -0
  2. {dotted_notation-0.44.3/dotted_notation.egg-info → dotted_notation-0.44.5}/PKG-INFO +1 -1
  3. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/__init__.py +2 -0
  4. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/api.py +36 -11
  5. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/engine.py +42 -0
  6. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/matchers.py +9 -2
  7. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/results.py +33 -5
  8. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/utils.py +19 -0
  9. {dotted_notation-0.44.3 → dotted_notation-0.44.5/dotted_notation.egg-info}/PKG-INFO +1 -1
  10. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/pyproject.toml +1 -1
  11. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/LICENSE +0 -0
  12. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/MANIFEST.in +0 -0
  13. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/README.md +0 -0
  14. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/__main__.py +0 -0
  15. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/access.py +0 -0
  16. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/base.py +0 -0
  17. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/cli/__init__.py +0 -0
  18. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/cli/_compat.py +0 -0
  19. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/cli/formats.py +0 -0
  20. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/cli/main.py +0 -0
  21. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/containers.py +0 -0
  22. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/filters.py +0 -0
  23. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/grammar.py +0 -0
  24. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/groups.py +0 -0
  25. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/predicates.py +0 -0
  26. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/recursive.py +0 -0
  27. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/sql/__init__.py +0 -0
  28. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/sql/core.py +0 -0
  29. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/sql/pg.py +0 -0
  30. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/transforms.py +0 -0
  31. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/utypes.py +0 -0
  32. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted/wrappers.py +0 -0
  33. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted_notation.egg-info/SOURCES.txt +0 -0
  34. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted_notation.egg-info/dependency_links.txt +0 -0
  35. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted_notation.egg-info/entry_points.txt +0 -0
  36. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted_notation.egg-info/requires.txt +0 -0
  37. {dotted_notation-0.44.3 → dotted_notation-0.44.5}/dotted_notation.egg-info/top_level.txt +0 -0
  38. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.44.3
3
+ Version: 0.44.5
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: MIT
@@ -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 _is_simple(parsed)
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
- @property
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
- @property
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
- try:
76
- return hash((self.ops, self.transforms, self.guard, self.guard_op))
77
- except TypeError:
78
- return hash((self.ops, Dotted._hashable(self.transforms), self.guard, self.guard_op))
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.44.3
3
+ Version: 0.44.5
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: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dotted_notation"
7
- version = "0.44.3"
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"