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.
- {dotted_notation-0.43.0/dotted_notation.egg-info → dotted_notation-0.43.1}/PKG-INFO +1 -1
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/matchers.py +23 -8
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/sqlize.py +50 -5
- {dotted_notation-0.43.0 → dotted_notation-0.43.1/dotted_notation.egg-info}/PKG-INFO +1 -1
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/setup.py +1 -1
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_named_subst.py +34 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_sqlize.py +53 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/LICENSE +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/README.md +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/__init__.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/__main__.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/access.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/api.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/base.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/cli/main.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/containers.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/engine.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/filters.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/grammar.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/groups.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/predicates.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/recursive.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/results.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/transforms.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/utils.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/utypes.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted/wrappers.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/setup.cfg +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/__init__.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_api.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_appender.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_assemble.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_attrs.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_bindings.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_cli.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_concat.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_container_filter.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_cut.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_empty.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_filter_keyvalue.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_get.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_guard_transforms.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_invert.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_keys_values.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_match.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_matchable.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_negation.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_nop.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_numeric.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_opgroup.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_pluck.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_predicates.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_quote_idempotent.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_recursive.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_reference.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_replace.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_slice.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_softcut.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_strict.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_string_glob.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_subst_escape.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_subst_transforms.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_threading.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_transforms.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_translate.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_type_restriction.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_unpack.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_update.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_update_if.py +0 -0
- {dotted_notation-0.43.0 → dotted_notation-0.43.1}/tests/test_value_guard.py +0 -0
|
@@ -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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
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
|
-
|
|
99
|
-
if
|
|
100
|
-
self.missing.append(
|
|
101
|
-
return
|
|
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:
|
|
@@ -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.
|
|
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)')
|
|
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
|
|
File without changes
|
{dotted_notation-0.43.0 → dotted_notation-0.43.1}/dotted_notation.egg-info/dependency_links.txt
RENAMED
|
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
|
|
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
|
|
File without changes
|