dotted-notation 0.43.3__tar.gz → 0.43.4__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 (79) hide show
  1. {dotted_notation-0.43.3/dotted_notation.egg-info → dotted_notation-0.43.4}/PKG-INFO +9 -1
  2. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/README.md +8 -0
  3. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/access.py +8 -0
  4. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/filters.py +44 -0
  5. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/matchers.py +5 -1
  6. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/wrappers.py +25 -0
  7. {dotted_notation-0.43.3 → dotted_notation-0.43.4/dotted_notation.egg-info}/PKG-INFO +9 -1
  8. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/setup.py +1 -1
  9. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_named_subst.py +74 -0
  10. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/LICENSE +0 -0
  11. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/__init__.py +0 -0
  12. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/__main__.py +0 -0
  13. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/api.py +0 -0
  14. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/base.py +0 -0
  15. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/cli/__init__.py +0 -0
  16. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/cli/_compat.py +0 -0
  17. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/cli/formats.py +0 -0
  18. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/cli/main.py +0 -0
  19. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/containers.py +0 -0
  20. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/engine.py +0 -0
  21. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/grammar.py +0 -0
  22. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/groups.py +0 -0
  23. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/predicates.py +0 -0
  24. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/recursive.py +0 -0
  25. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/results.py +0 -0
  26. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/sqlize.py +0 -0
  27. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/transforms.py +0 -0
  28. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/utils.py +0 -0
  29. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted/utypes.py +0 -0
  30. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted_notation.egg-info/SOURCES.txt +0 -0
  31. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted_notation.egg-info/dependency_links.txt +0 -0
  32. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted_notation.egg-info/entry_points.txt +0 -0
  33. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted_notation.egg-info/requires.txt +0 -0
  34. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/dotted_notation.egg-info/top_level.txt +0 -0
  35. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/setup.cfg +0 -0
  36. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/__init__.py +0 -0
  37. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_api.py +0 -0
  38. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_appender.py +0 -0
  39. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_assemble.py +0 -0
  40. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_attrs.py +0 -0
  41. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_bindings.py +0 -0
  42. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_cli.py +0 -0
  43. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_concat.py +0 -0
  44. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_container_filter.py +0 -0
  45. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_cut.py +0 -0
  46. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_empty.py +0 -0
  47. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_filter_keyvalue.py +0 -0
  48. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_get.py +0 -0
  49. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_guard_transforms.py +0 -0
  50. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_invert.py +0 -0
  51. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_json_sentinels.py +0 -0
  52. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_keys_values.py +0 -0
  53. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_match.py +0 -0
  54. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_matchable.py +0 -0
  55. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_negation.py +0 -0
  56. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_nop.py +0 -0
  57. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_numeric.py +0 -0
  58. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_opgroup.py +0 -0
  59. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_pluck.py +0 -0
  60. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_predicates.py +0 -0
  61. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_quote_idempotent.py +0 -0
  62. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_recursive.py +0 -0
  63. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_reference.py +0 -0
  64. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_replace.py +0 -0
  65. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_slice.py +0 -0
  66. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_softcut.py +0 -0
  67. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_sqlize.py +0 -0
  68. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_strict.py +0 -0
  69. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_string_glob.py +0 -0
  70. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_subst_escape.py +0 -0
  71. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_subst_transforms.py +0 -0
  72. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_threading.py +0 -0
  73. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_transforms.py +0 -0
  74. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_translate.py +0 -0
  75. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_type_restriction.py +0 -0
  76. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_unpack.py +0 -0
  77. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_update.py +0 -0
  78. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_update_if.py +0 -0
  79. {dotted_notation-0.43.3 → dotted_notation-0.43.4}/tests/test_value_guard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.43.3
3
+ Version: 0.43.4
4
4
  Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
5
5
  Home-page: https://github.com/freywaid/dotted
6
6
  Author: Frey Waid
@@ -244,6 +244,14 @@ Several Python libraries handle nested data access. Here's how dotted compares:
244
244
  <a id="breaking-changes"></a>
245
245
  ## Breaking Changes
246
246
 
247
+ ### v0.43.4
248
+ - **`is_pattern` no longer returns `True` for substitutions**. `Subst` previously
249
+ inherited from `Pattern` in the class hierarchy, so any path containing a
250
+ substitution in an access position (e.g. `a.$(x)`, `a.$0`) reported as a
251
+ pattern. Substitutions resolve to a single key, not a set — that was a
252
+ misclassification. Use `is_template(path)` to check "contains a
253
+ substitution."
254
+
247
255
  ### v0.40.0
248
256
  - **`unpack()` now returns a `dict`**: Previously returned a tuple of
249
257
  `(path, value)` pairs. Now returns `{path: value, ...}` directly.
@@ -207,6 +207,14 @@ Several Python libraries handle nested data access. Here's how dotted compares:
207
207
  <a id="breaking-changes"></a>
208
208
  ## Breaking Changes
209
209
 
210
+ ### v0.43.4
211
+ - **`is_pattern` no longer returns `True` for substitutions**. `Subst` previously
212
+ inherited from `Pattern` in the class hierarchy, so any path containing a
213
+ substitution in an access position (e.g. `a.$(x)`, `a.$0`) reported as a
214
+ pattern. Substitutions resolve to a single key, not a set — that was a
215
+ misclassification. Use `is_template(path)` to check "contains a
216
+ substitution."
217
+
210
218
  ### v0.40.0
211
219
  - **`unpack()` now returns a `dict`**: Previously returned a tuple of
212
220
  `(path, value)` pairs. Now returns `{path: value, ...}` directly.
@@ -862,6 +862,14 @@ class SliceFilter(BaseOp):
862
862
  def is_pattern(self):
863
863
  return False
864
864
 
865
+ def is_template(self):
866
+ """
867
+ True if any attached filter contains a substitution reference.
868
+ """
869
+ return any(
870
+ hasattr(f, 'is_template') and f.is_template()
871
+ for f in self.filters)
872
+
865
873
  def is_empty(self, node):
866
874
  return not node
867
875
 
@@ -6,6 +6,13 @@ from . import predicates
6
6
  from .access import Slot, Slice
7
7
 
8
8
 
9
+ def _any_template(items):
10
+ """
11
+ True if any item in an iterable has is_template() returning True.
12
+ """
13
+ return any(hasattr(x, 'is_template') and x.is_template() for x in items)
14
+
15
+
9
16
  class FilterOp(base.MatchOp):
10
17
  def is_pattern(self):
11
18
  return False
@@ -50,6 +57,12 @@ class FilterKey(base.MatchOp):
50
57
  def is_dotted(self):
51
58
  return len(self.parts) > 1
52
59
 
60
+ def is_template(self):
61
+ """
62
+ True if any part of the filter key contains a substitution reference.
63
+ """
64
+ return _any_template(self.parts)
65
+
53
66
  def get_values(self, node):
54
67
  """
55
68
  Get all values from node matching this key, traversing dotted path if needed.
@@ -188,6 +201,16 @@ class FilterKeyValue(FilterOp):
188
201
  t_str = ''.join('|' + t.operator() for t in self.transforms) if self.transforms else ''
189
202
  return f'{self.key}{t_str}{self._eq_str}{self.val}'
190
203
 
204
+ def is_template(self):
205
+ """
206
+ True if the key, value, or any transform contains a substitution.
207
+ """
208
+ if hasattr(self.key, 'is_template') and self.key.is_template():
209
+ return True
210
+ if hasattr(self.val, 'is_template') and self.val.is_template():
211
+ return True
212
+ return _any_template(self.transforms)
213
+
191
214
  def _eq_match(self, node):
192
215
  """
193
216
  True if any value from self.key in node matches self.val (after transforms).
@@ -351,6 +374,11 @@ class FilterGroup(FilterOp):
351
374
  return None
352
375
  return self.inner.match(op.inner)
353
376
 
377
+ def is_template(self):
378
+ return (self.inner is not None
379
+ and hasattr(self.inner, 'is_template')
380
+ and self.inner.is_template())
381
+
354
382
  def resolve(self, bindings, partial=False):
355
383
  """
356
384
  Resolve $N in inner filter.
@@ -403,6 +431,9 @@ class FilterAnd(FilterOp):
403
431
  results.append(m)
404
432
  return FilterAnd(*results)
405
433
 
434
+ def is_template(self):
435
+ return _any_template(self.filters)
436
+
406
437
  def resolve(self, bindings, partial=False):
407
438
  """
408
439
  Resolve $N in each filter.
@@ -443,6 +474,9 @@ class FilterOr(FilterOp):
443
474
  seen.add(item_id)
444
475
  yield item
445
476
 
477
+ def is_template(self):
478
+ return _any_template(self.filters)
479
+
446
480
  def resolve(self, bindings, partial=False):
447
481
  """
448
482
  Resolve $N in each filter.
@@ -488,6 +522,11 @@ class FilterKeyValueFirst(FilterOp):
488
522
  return self.inner.match(op.inner)
489
523
  return None
490
524
 
525
+ def is_template(self):
526
+ return (self.inner is not None
527
+ and hasattr(self.inner, 'is_template')
528
+ and self.inner.is_template())
529
+
491
530
  def resolve(self, bindings, partial=False):
492
531
  """
493
532
  Resolve $N in inner filter.
@@ -541,6 +580,11 @@ class FilterNot(FilterOp):
541
580
  return None
542
581
  return self.inner.match(op.inner)
543
582
 
583
+ def is_template(self):
584
+ return (self.inner is not None
585
+ and hasattr(self.inner, 'is_template')
586
+ and self.inner.is_template())
587
+
544
588
  def resolve(self, bindings, partial=False):
545
589
  """
546
590
  Resolve $N in inner filter.
@@ -150,12 +150,16 @@ class Pattern(MatchOp):
150
150
  return True
151
151
 
152
152
 
153
- class Subst(Pattern):
153
+ class Subst(MatchOp):
154
154
  """
155
155
  Substitution op: $0, $(name), $(0|transform), $(name|int), etc.
156
156
  Resolves against bindings via __getitem__: list for positional,
157
157
  dict for named or numeric keys. Optional transforms applied
158
158
  after lookup.
159
+
160
+ Not a Pattern subclass — substitutions resolve to a single key, not
161
+ a set of keys. is_template() is the right check for "contains a
162
+ substitution".
159
163
  """
160
164
  def __init__(self, *args, transforms=(), **kwargs):
161
165
  super().__init__(*args, **kwargs)
@@ -202,6 +202,20 @@ class ValueGuard(Wrap):
202
202
  def is_pattern(self):
203
203
  return self.inner.is_pattern()
204
204
 
205
+ def is_template(self):
206
+ """
207
+ True if the inner op, the guard value, or any transform contains
208
+ a substitution reference.
209
+ """
210
+ if self.inner.is_template():
211
+ return True
212
+ if hasattr(self.guard, 'is_template') and self.guard.is_template():
213
+ return True
214
+ for t in self.transforms:
215
+ if hasattr(t, 'is_template') and t.is_template():
216
+ return True
217
+ return False
218
+
205
219
  def is_recursive(self):
206
220
  return self.inner.is_recursive()
207
221
 
@@ -419,6 +433,17 @@ class FilterWrap(Wrap):
419
433
  return s[:-1] + filter_str + ']'
420
434
  return s + filter_str
421
435
 
436
+ def is_template(self):
437
+ """
438
+ True if the inner op or any attached filter contains a
439
+ substitution reference.
440
+ """
441
+ if self.inner.is_template():
442
+ return True
443
+ return any(
444
+ hasattr(f, 'is_template') and f.is_template()
445
+ for f in self.filters)
446
+
422
447
  def default(self):
423
448
  """
424
449
  Filters imply the value has structure to check against,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.43.3
3
+ Version: 0.43.4
4
4
  Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
5
5
  Home-page: https://github.com/freywaid/dotted
6
6
  Author: Frey Waid
@@ -244,6 +244,14 @@ Several Python libraries handle nested data access. Here's how dotted compares:
244
244
  <a id="breaking-changes"></a>
245
245
  ## Breaking Changes
246
246
 
247
+ ### v0.43.4
248
+ - **`is_pattern` no longer returns `True` for substitutions**. `Subst` previously
249
+ inherited from `Pattern` in the class hierarchy, so any path containing a
250
+ substitution in an access position (e.g. `a.$(x)`, `a.$0`) reported as a
251
+ pattern. Substitutions resolve to a single key, not a set — that was a
252
+ misclassification. Use `is_template(path)` to check "contains a
253
+ substitution."
254
+
247
255
  ### v0.40.0
248
256
  - **`unpack()` now returns a `dict`**: Previously returned a tuple of
249
257
  `(path, value)` pairs. Now returns `{path: value, ...}` directly.
@@ -5,7 +5,7 @@ with open("README.md", "rt") as f:
5
5
 
6
6
  setuptools.setup(
7
7
  name="dotted_notation",
8
- version="0.43.3",
8
+ version="0.43.4",
9
9
  author="Frey Waid",
10
10
  author_email="logophage1@gmail.com",
11
11
  description="Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms",
@@ -106,6 +106,80 @@ def test_is_template_named_escaped():
106
106
  assert dotted.is_template('\\$(name)') is False
107
107
 
108
108
 
109
+ # ---- is_template catches substs in all positions ----
110
+
111
+ def test_is_template_guard_value():
112
+ # Subst as guard RHS (previously missed)
113
+ assert dotted.is_template('a=$(x)') is True
114
+ assert dotted.is_template('a>=$(x)') is True
115
+ assert dotted.is_template('a!=$(x)') is True
116
+
117
+
118
+ def test_is_template_guard_transform():
119
+ # Subst combined with guard cast
120
+ assert dotted.is_template('a|int=$(x)') is True
121
+
122
+
123
+ def test_is_template_filter_wrap_value():
124
+ # Subst inside filter-wrap predicate value
125
+ assert dotted.is_template('a[*&x=$(y)]') is True
126
+
127
+
128
+ def test_is_template_filter_wrap_key():
129
+ # Subst as filter key
130
+ assert dotted.is_template('a[$(k)=1]') is True
131
+
132
+
133
+ def test_is_template_filter_wrap_transform():
134
+ # Subst with filter transform
135
+ assert dotted.is_template('a[*&x|int=$(y)]') is True
136
+
137
+
138
+ def test_is_template_filter_negation():
139
+ # Subst under filter negation
140
+ assert dotted.is_template('a[!x=$(y)]') is True
141
+
142
+
143
+ def test_is_template_filter_group():
144
+ # Subst inside filter group (AND/OR of predicates)
145
+ assert dotted.is_template('a[*&(b=$(x) & c=1)]') is True
146
+
147
+
148
+ def test_is_template_slice_filter():
149
+ # SliceFilter (compact [filter] form) carrying a subst
150
+ assert dotted.is_template('a[$(k)=1]') is True
151
+
152
+
153
+ def test_is_template_group_branch():
154
+ # Subst in an access-op group branch
155
+ assert dotted.is_template('(a=$(x) & b=1)') is True
156
+
157
+
158
+ def test_is_template_false_for_refs():
159
+ # References are not templates (resolve against data, not bindings)
160
+ assert dotted.is_template('a.$$(b)') is False
161
+ assert dotted.is_template('a=$$(b)') is False
162
+
163
+
164
+ def test_is_template_false_for_patterns():
165
+ # Plain patterns are not templates
166
+ assert dotted.is_template('a.*.b') is False
167
+ assert dotted.is_template('a=/re/') is False
168
+
169
+
170
+ # ---- is_pattern: substitutions are NOT patterns (single-yield) ----
171
+
172
+ def test_is_pattern_false_for_subst_in_access():
173
+ # $(x) as an access-position matcher is not a pattern (yields one key).
174
+ assert dotted.is_pattern('a.$(x)') is False
175
+ assert dotted.is_pattern('a.$0') is False
176
+
177
+
178
+ def test_is_pattern_false_for_subst_in_guard():
179
+ # Concrete access with subst guard is still not a pattern
180
+ assert dotted.is_pattern('a=$(x)') is False
181
+
182
+
109
183
  # ---- quote ----
110
184
 
111
185
  def test_quote_named_subst_literal():