dotted-notation 0.43.0__tar.gz → 0.43.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 (78) hide show
  1. {dotted_notation-0.43.0/dotted_notation.egg-info → dotted_notation-0.43.1}/PKG-INFO +1 -1
  2. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/matchers.py +23 -8
  3. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/sqlize.py +50 -5
  4. {dotted_notation-0.43.0 → dotted_notation-0.43.1/dotted_notation.egg-info}/PKG-INFO +1 -1
  5. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/setup.py +1 -1
  6. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_named_subst.py +34 -0
  7. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_sqlize.py +53 -0
  8. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/LICENSE +0 -0
  9. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/README.md +0 -0
  10. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/__init__.py +0 -0
  11. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/__main__.py +0 -0
  12. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/access.py +0 -0
  13. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/api.py +0 -0
  14. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/base.py +0 -0
  15. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/cli/__init__.py +0 -0
  16. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/cli/_compat.py +0 -0
  17. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/cli/formats.py +0 -0
  18. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/cli/main.py +0 -0
  19. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/containers.py +0 -0
  20. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/engine.py +0 -0
  21. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/filters.py +0 -0
  22. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/grammar.py +0 -0
  23. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/groups.py +0 -0
  24. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/predicates.py +0 -0
  25. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/recursive.py +0 -0
  26. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/results.py +0 -0
  27. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/transforms.py +0 -0
  28. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/utils.py +0 -0
  29. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/utypes.py +0 -0
  30. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/wrappers.py +0 -0
  31. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted_notation.egg-info/SOURCES.txt +0 -0
  32. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted_notation.egg-info/dependency_links.txt +0 -0
  33. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted_notation.egg-info/entry_points.txt +0 -0
  34. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted_notation.egg-info/requires.txt +0 -0
  35. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted_notation.egg-info/top_level.txt +0 -0
  36. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/setup.cfg +0 -0
  37. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/__init__.py +0 -0
  38. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_api.py +0 -0
  39. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_appender.py +0 -0
  40. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_assemble.py +0 -0
  41. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_attrs.py +0 -0
  42. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_bindings.py +0 -0
  43. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_cli.py +0 -0
  44. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_concat.py +0 -0
  45. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_container_filter.py +0 -0
  46. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_cut.py +0 -0
  47. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_empty.py +0 -0
  48. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_filter_keyvalue.py +0 -0
  49. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_get.py +0 -0
  50. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_guard_transforms.py +0 -0
  51. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_invert.py +0 -0
  52. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_keys_values.py +0 -0
  53. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_match.py +0 -0
  54. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_matchable.py +0 -0
  55. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_negation.py +0 -0
  56. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_nop.py +0 -0
  57. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_numeric.py +0 -0
  58. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_opgroup.py +0 -0
  59. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_pluck.py +0 -0
  60. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_predicates.py +0 -0
  61. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_quote_idempotent.py +0 -0
  62. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_recursive.py +0 -0
  63. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_reference.py +0 -0
  64. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_replace.py +0 -0
  65. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_slice.py +0 -0
  66. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_softcut.py +0 -0
  67. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_strict.py +0 -0
  68. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_string_glob.py +0 -0
  69. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_subst_escape.py +0 -0
  70. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_subst_transforms.py +0 -0
  71. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_threading.py +0 -0
  72. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_transforms.py +0 -0
  73. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_translate.py +0 -0
  74. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_type_restriction.py +0 -0
  75. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_unpack.py +0 -0
  76. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_update.py +0 -0
  77. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_update_if.py +0 -0
  78. {dotted_notation-0.43.0 → dotted_notation-0.43.1}/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.0
3
+ Version: 0.43.1
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
@@ -13,6 +13,9 @@ from .base import MatchOp
13
13
  from .utypes import ANY
14
14
 
15
15
 
16
+ _MISSING = object()
17
+
18
+
16
19
  class Const(MatchOp):
17
20
  _match_from = ('Const',)
18
21
 
@@ -190,14 +193,26 @@ class Subst(Pattern):
190
193
  def resolve(self, bindings, partial=False):
191
194
  """
192
195
  Resolve this substitution against bindings.
193
- """
194
- try:
195
- val = self._apply_transforms(bindings[self.value])
196
- return ResolvedValue(val)
197
- except (KeyError, IndexError, TypeError):
198
- if partial:
199
- return self
200
- raise
196
+
197
+ For string names, honor dotted notation for nested lookup: `$(a.b)`
198
+ resolves to `bindings['a']['b']` when present. Falls back to a
199
+ literal `bindings[name]` lookup (preserves the rare case of a key
200
+ that actually contains a dot). Integer names always use positional
201
+ lookup via __getitem__.
202
+ """
203
+ name = self.value
204
+ val = _MISSING
205
+ if isinstance(name, str):
206
+ from .api import get
207
+ val = get(bindings, name, default=_MISSING)
208
+ if val is _MISSING:
209
+ try:
210
+ val = bindings[name]
211
+ except (KeyError, IndexError, TypeError):
212
+ if partial:
213
+ return self
214
+ raise
215
+ return ResolvedValue(self._apply_transforms(val))
201
216
 
202
217
  def __repr__(self):
203
218
  suffix = self._transform_suffix()
@@ -46,6 +46,46 @@ def _quote_ident(name):
46
46
  return '"' + name.replace('"', '""') + '"'
47
47
 
48
48
 
49
+ # Subst names may themselves be dotted paths. To survive as SQL bind
50
+ # parameter names (which must be plain identifiers), the access operators
51
+ # are encoded with mnemonic tokens. This preserves the distinction
52
+ # between `a.b`, `a@b`, and `a[0]` in the placeholder name.
53
+ _OP_ENCODE = {
54
+ '.': '_dot_',
55
+ '@': '_at_',
56
+ '[': '_br_',
57
+ ']': '',
58
+ }
59
+
60
+
61
+ def _encode_subst_name(name):
62
+ """
63
+ Encode a subst name (a dotted path) into a plain SQL identifier.
64
+ Recognised access ops become mnemonic tokens; unsupported characters
65
+ raise TranslationError.
66
+ """
67
+ if _IDENT_RE.fullmatch(name):
68
+ return name
69
+ out = []
70
+ for c in name:
71
+ if c in _OP_ENCODE:
72
+ out.append(_OP_ENCODE[c])
73
+ elif c.isalnum() or c == '_':
74
+ out.append(c)
75
+ else:
76
+ raise TranslationError(
77
+ f'cannot encode character {c!r} in substitution name {name!r}; '
78
+ 'resolve via bindings= at sqlize time'
79
+ )
80
+ encoded = ''.join(out)
81
+ if not _IDENT_RE.fullmatch(encoded):
82
+ raise TranslationError(
83
+ f'substitution name {name!r} does not encode to a valid '
84
+ 'SQL identifier'
85
+ )
86
+ return encoded
87
+
88
+
49
89
  def _pg_path_segment(seg):
50
90
  """
51
91
  Escape a segment for a Postgres text[] path literal like '{a,b,c}'.
@@ -93,12 +133,17 @@ class _ParamState:
93
133
  def hoist_named(self, name):
94
134
  """
95
135
  Hoist a named substitution. If a value is supplied later it lands
96
- under `name`; until then the name is recorded in `missing`.
136
+ under the encoded name; until then it's recorded in `missing`.
137
+
138
+ Names that already are plain identifiers are used as-is. Dotted
139
+ paths (`$(user.age)`, `$(users[0].name)`, `$(obj@attr)`) are
140
+ encoded into plain identifiers using mnemonic tokens for access
141
+ ops so they can serve as SQL bind-parameter names.
97
142
  """
98
- name = str(name)
99
- if name not in self.params and name not in self.missing:
100
- self.missing.append(name)
101
- return name
143
+ encoded = _encode_subst_name(str(name))
144
+ if encoded not in self.params and encoded not in self.missing:
145
+ self.missing.append(encoded)
146
+ return encoded
102
147
 
103
148
 
104
149
  class _Translator:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.43.0
3
+ Version: 0.43.1
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
@@ -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.0",
8
+ version="0.43.1",
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",
@@ -62,6 +62,40 @@ def test_replace_named_missing_raises():
62
62
  replace('$(name)', {})
63
63
 
64
64
 
65
+ # ---- dotted-path lookup in named subst ----
66
+
67
+ def test_replace_dotted_subst():
68
+ # $(a.b) resolves via nested lookup in bindings
69
+ assert replace('prefix.$(a.b)', {'a': {'b': 'val'}}) == 'prefix.val'
70
+
71
+
72
+ def test_replace_dotted_subst_deep():
73
+ assert replace('$(x.y.z)', {'x': {'y': {'z': 42}}}) == '42'
74
+
75
+
76
+ def test_replace_dotted_subst_falls_back_to_literal():
77
+ # When nested lookup finds nothing, a literal dotted key still works
78
+ assert replace('$(a.b)', {'a.b': 'literal'}) == 'literal'
79
+
80
+
81
+ def test_replace_dotted_subst_nested_wins_over_literal():
82
+ # Nested match is preferred over literal key
83
+ assert replace('$(a.b)', {'a.b': 'literal', 'a': {'b': 'nested'}}) == 'nested'
84
+
85
+
86
+ def test_replace_dotted_subst_missing_raises():
87
+ with pytest.raises(KeyError):
88
+ replace('$(a.b)', {'a': {}})
89
+
90
+
91
+ def test_replace_dotted_subst_partial():
92
+ assert replace('$(a.b)', {}, partial=True) == '$(a.b)'
93
+
94
+
95
+ def test_replace_dotted_subst_with_transform():
96
+ assert replace('$(a.b|int)', {'a': {'b': '42'}}) == '42'
97
+
98
+
65
99
  # ---- is_template ----
66
100
 
67
101
  def test_is_template_named():
@@ -326,11 +326,64 @@ def test_mixed_subst_and_literal():
326
326
  }
327
327
 
328
328
 
329
+ def test_value_sub_dotted_name_with_bindings():
330
+ # Dotted subst name resolves via nested lookup in bindings
331
+ result = sqlize('age>=$(user.min_age)',
332
+ bindings={'user': {'min_age': 30}})
333
+ assert result == {
334
+ 'select': 'age',
335
+ 'where': 'age >= :_p1',
336
+ 'params': {'_p1': 30},
337
+ }
338
+
339
+
340
+ def test_value_sub_dotted_name_encoded():
341
+ # Dotted subst name is encoded into a plain SQL identifier.
342
+ result = sqlize('age>=$(user.min_age)')
343
+ assert result == {
344
+ 'select': 'age',
345
+ 'where': 'age >= :user_dot_min_age',
346
+ 'params': {},
347
+ 'missing': ['user_dot_min_age'],
348
+ }
349
+
350
+
351
+ def test_value_sub_attr_name_encoded():
352
+ result = sqlize('age>=$(obj@attr)')
353
+ assert result['where'] == 'age >= :obj_at_attr'
354
+ assert result['missing'] == ['obj_at_attr']
355
+
356
+
357
+ def test_value_sub_slot_name_encoded():
358
+ result = sqlize('age>=$(arr[0])')
359
+ assert result['where'] == 'age >= :arr_br_0'
360
+ assert result['missing'] == ['arr_br_0']
361
+
362
+
363
+ def test_value_sub_mixed_path_name_encoded():
364
+ result = sqlize('age>=$(users[0].name)')
365
+ assert result['where'] == 'age >= :users_br_0_dot_name'
366
+ assert result['missing'] == ['users_br_0_dot_name']
367
+
368
+
369
+ def test_value_sub_unencodable_char_errors():
370
+ # Characters outside the supported op set raise
371
+ with pytest.raises(TranslationError):
372
+ sqlize('age>=$(user age)')
373
+
374
+
329
375
  def test_path_sub_with_binding():
330
376
  result = sqlize('data.$(field)', bindings={'field': 'name'})
331
377
  assert result == {'select': "data #>> '{name}'", 'params': {}}
332
378
 
333
379
 
380
+ def test_path_sub_dotted_name_with_bindings():
381
+ # Dotted subst name in path position resolves via nested lookup
382
+ result = sqlize('data.$(cfg.field)',
383
+ bindings={'cfg': {'field': 'name'}})
384
+ assert result == {'select': "data #>> '{name}'", 'params': {}}
385
+
386
+
334
387
  def test_path_sub_without_binding_errors():
335
388
  with pytest.raises(TranslationError):
336
389
  sqlize('data.$(field)')