dotted-notation 0.43.1__tar.gz → 0.43.2__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.1/dotted_notation.egg-info → dotted_notation-0.43.2}/PKG-INFO +1 -1
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/grammar.py +3 -3
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/matchers.py +1 -1
- {dotted_notation-0.43.1 → dotted_notation-0.43.2/dotted_notation.egg-info}/PKG-INFO +1 -1
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted_notation.egg-info/SOURCES.txt +1 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/setup.py +1 -1
- dotted_notation-0.43.2/tests/test_cli.py +515 -0
- dotted_notation-0.43.2/tests/test_json_sentinels.py +62 -0
- dotted_notation-0.43.2/tests/test_strict.py +314 -0
- dotted_notation-0.43.2/tests/test_value_guard.py +280 -0
- dotted_notation-0.43.1/tests/test_cli.py +0 -551
- dotted_notation-0.43.1/tests/test_strict.py +0 -323
- dotted_notation-0.43.1/tests/test_value_guard.py +0 -285
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/LICENSE +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/README.md +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/__init__.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/__main__.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/access.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/api.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/base.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/cli/main.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/containers.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/engine.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/filters.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/groups.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/predicates.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/recursive.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/results.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/sqlize.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/transforms.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/utils.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/utypes.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/wrappers.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/setup.cfg +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/__init__.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_api.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_appender.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_assemble.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_attrs.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_bindings.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_concat.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_container_filter.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_cut.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_empty.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_filter_keyvalue.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_get.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_guard_transforms.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_invert.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_keys_values.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_match.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_matchable.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_named_subst.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_negation.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_nop.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_numeric.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_opgroup.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_pluck.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_predicates.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_quote_idempotent.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_recursive.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_reference.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_replace.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_slice.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_softcut.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_sqlize.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_string_glob.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_subst_escape.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_subst_transforms.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_threading.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_transforms.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_translate.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_type_restriction.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_unpack.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_update.py +0 -0
- {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_update_if.py +0 -0
|
@@ -69,9 +69,9 @@ transform_name = pp.Word(pp.alphas + '_', pp.alphanums + '_.')
|
|
|
69
69
|
quoted = pp.QuotedString('"', esc_char='\\') | pp.QuotedString("'", esc_char='\\')
|
|
70
70
|
plus = pp.Literal('+')
|
|
71
71
|
integer = ppc.signed_integer
|
|
72
|
-
none = pp.Literal('None').set_parse_action(matchers.NoneValue)
|
|
73
|
-
true = pp.Literal('True').set_parse_action(matchers.Boolean)
|
|
74
|
-
false = pp.Literal('False').set_parse_action(matchers.Boolean)
|
|
72
|
+
none = (pp.Literal('None') | pp.Literal('null')).set_parse_action(matchers.NoneValue)
|
|
73
|
+
true = (pp.Literal('True') | pp.Literal('true')).set_parse_action(matchers.Boolean)
|
|
74
|
+
false = (pp.Literal('False') | pp.Literal('false')).set_parse_action(matchers.Boolean)
|
|
75
75
|
|
|
76
76
|
reserved = '.[]*:|+?/=,@&()!~#{}$<>' # {} for container syntax, $ for substitution, <> for comparisons
|
|
77
77
|
breserved = ''.join('\\' + i for i in reserved)
|
|
@@ -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.2",
|
|
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",
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from dotted.cli.main import main as _dq_main
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def dq(*args, input_text='', expect_fail=False):
|
|
14
|
+
"""
|
|
15
|
+
Run dq in-process by calling main(argv) with captured I/O.
|
|
16
|
+
"""
|
|
17
|
+
argv = list(args)
|
|
18
|
+
stdin_backup = sys.stdin
|
|
19
|
+
stdout_backup = sys.stdout
|
|
20
|
+
stderr_backup = sys.stderr
|
|
21
|
+
sys.stdin = io.StringIO(input_text)
|
|
22
|
+
sys.stdout = io.StringIO()
|
|
23
|
+
sys.stderr = io.StringIO()
|
|
24
|
+
try:
|
|
25
|
+
_dq_main(argv)
|
|
26
|
+
returncode = 0
|
|
27
|
+
except SystemExit as e:
|
|
28
|
+
returncode = int(e.code) if isinstance(e.code, int) else 1
|
|
29
|
+
if isinstance(e.code, str):
|
|
30
|
+
sys.stderr.write(e.code + '\n')
|
|
31
|
+
finally:
|
|
32
|
+
stdout_val = sys.stdout.getvalue()
|
|
33
|
+
stderr_val = sys.stderr.getvalue()
|
|
34
|
+
sys.stdin = stdin_backup
|
|
35
|
+
sys.stdout = stdout_backup
|
|
36
|
+
sys.stderr = stderr_backup
|
|
37
|
+
|
|
38
|
+
if expect_fail:
|
|
39
|
+
assert returncode != 0, f"Expected failure but got: {stdout_val}"
|
|
40
|
+
return stderr_val
|
|
41
|
+
assert returncode == 0, f"dq failed: {stderr_val}"
|
|
42
|
+
return stdout_val.replace('\r\n', '\n')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def dq_subprocess(*args, **kwargs):
|
|
46
|
+
"""
|
|
47
|
+
Run dq via subprocess. Use for tests that need a real process (e.g. --version).
|
|
48
|
+
"""
|
|
49
|
+
return subprocess.run(
|
|
50
|
+
[sys.executable, '-m', 'dotted'] + list(args),
|
|
51
|
+
capture_output=True, text=True, **kwargs,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Get
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def test_single_path_positional():
|
|
60
|
+
out = dq('a.b', input_text='{"a": {"b": 1}}')
|
|
61
|
+
assert out.strip() == '1'
|
|
62
|
+
|
|
63
|
+
def test_single_path_flag():
|
|
64
|
+
out = dq('-p', 'a.b', input_text='{"a": {"b": 1}}')
|
|
65
|
+
assert out.strip() == '1'
|
|
66
|
+
|
|
67
|
+
def test_explicit_get():
|
|
68
|
+
out = dq('get', '-p', 'a.b', input_text='{"a": {"b": 1}}')
|
|
69
|
+
assert out.strip() == '1'
|
|
70
|
+
|
|
71
|
+
def test_multi_path_projection():
|
|
72
|
+
out = dq('-p', 'a', '-p', 'b', input_text='{"a": 1, "b": 2, "c": 3}')
|
|
73
|
+
assert json.loads(out) == {"a": 1, "b": 2}
|
|
74
|
+
|
|
75
|
+
def test_nested_projection():
|
|
76
|
+
out = dq('-p', 'a.x', '-p', 'b', input_text='{"a": {"x": 1, "y": 2}, "b": 3}')
|
|
77
|
+
assert json.loads(out) == {"a": {"x": 1}, "b": 3}
|
|
78
|
+
|
|
79
|
+
def test_unpack_projection():
|
|
80
|
+
out = dq('--unpack', '-p', 'a.x', '-p', 'b',
|
|
81
|
+
input_text='{"a": {"x": 1, "y": 2}, "b": 3}')
|
|
82
|
+
assert json.loads(out) == {"a.x": 1, "b": 3}
|
|
83
|
+
|
|
84
|
+
def test_get_string_value():
|
|
85
|
+
out = dq('name', input_text='{"name": "alice"}')
|
|
86
|
+
assert out.strip() == '"alice"'
|
|
87
|
+
|
|
88
|
+
def test_get_nested_object():
|
|
89
|
+
out = dq('a', input_text='{"a": {"b": 1, "c": 2}}')
|
|
90
|
+
assert json.loads(out) == {"b": 1, "c": 2}
|
|
91
|
+
|
|
92
|
+
def test_get_missing_key():
|
|
93
|
+
out = dq('missing', input_text='{"a": 1}')
|
|
94
|
+
assert out.strip() == 'null'
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Update
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def test_single_update():
|
|
102
|
+
out = dq('update', '-p', 'a', '42', input_text='{"a": 1, "b": 2}')
|
|
103
|
+
assert json.loads(out) == {"a": 42, "b": 2}
|
|
104
|
+
|
|
105
|
+
def test_multi_update():
|
|
106
|
+
out = dq('update', '-p', 'a', '42', '-p', 'b', '43',
|
|
107
|
+
input_text='{"a": 1, "b": 2, "c": 3}')
|
|
108
|
+
assert json.loads(out) == {"a": 42, "b": 43, "c": 3}
|
|
109
|
+
|
|
110
|
+
def test_update_string_value():
|
|
111
|
+
out = dq('update', '-p', 'name', '"bob"', input_text='{"name": "alice"}')
|
|
112
|
+
assert json.loads(out) == {"name": "bob"}
|
|
113
|
+
|
|
114
|
+
def test_update_nested():
|
|
115
|
+
out = dq('update', '-p', 'a.b', '99', input_text='{"a": {"b": 1}, "c": 2}')
|
|
116
|
+
assert json.loads(out) == {"a": {"b": 99}, "c": 2}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Remove
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def test_single_remove():
|
|
124
|
+
out = dq('remove', '-p', 'a', input_text='{"a": 1, "b": 2}')
|
|
125
|
+
assert json.loads(out) == {"b": 2}
|
|
126
|
+
|
|
127
|
+
def test_multi_remove():
|
|
128
|
+
out = dq('remove', '-p', 'a', '-p', 'b',
|
|
129
|
+
input_text='{"a": 1, "b": 2, "c": 3}')
|
|
130
|
+
assert json.loads(out) == {"c": 3}
|
|
131
|
+
|
|
132
|
+
def test_remove_nested():
|
|
133
|
+
out = dq('remove', '-p', 'a.b', input_text='{"a": {"b": 1, "c": 2}}')
|
|
134
|
+
assert json.loads(out) == {"a": {"c": 2}}
|
|
135
|
+
|
|
136
|
+
def test_remove_conditional_match():
|
|
137
|
+
out = dq('remove', '-p', 'a', '1', input_text='{"a": 1, "b": 2}')
|
|
138
|
+
assert json.loads(out) == {"b": 2}
|
|
139
|
+
|
|
140
|
+
def test_remove_conditional_no_match():
|
|
141
|
+
out = dq('remove', '-p', 'a', '99', input_text='{"a": 1, "b": 2}')
|
|
142
|
+
assert json.loads(out) == {"a": 1, "b": 2}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# JSONL
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def test_jsonl_get():
|
|
150
|
+
inp = '{"a":1,"b":2}\n{"a":3,"b":4}\n'
|
|
151
|
+
out = dq('-i', 'jsonl', '-p', 'a', input_text=inp)
|
|
152
|
+
lines = [l for l in out.strip().split('\n') if l]
|
|
153
|
+
assert lines == ['1', '3']
|
|
154
|
+
|
|
155
|
+
def test_jsonl_update():
|
|
156
|
+
inp = '{"a":1}\n{"a":2}\n'
|
|
157
|
+
out = dq('-i', 'jsonl', 'update', '-p', 'a', '99', input_text=inp)
|
|
158
|
+
lines = [json.loads(l) for l in out.strip().split('\n') if l]
|
|
159
|
+
assert lines == [{"a": 99}, {"a": 99}]
|
|
160
|
+
|
|
161
|
+
def test_jsonl_remove():
|
|
162
|
+
inp = '{"a":1,"b":2}\n{"a":3,"b":4}\n'
|
|
163
|
+
out = dq('-i', 'jsonl', 'remove', '-p', 'a', input_text=inp)
|
|
164
|
+
lines = [json.loads(l) for l in out.strip().split('\n') if l]
|
|
165
|
+
assert lines == [{"b": 2}, {"b": 4}]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# JSON array streaming
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def test_json_array_streams_elements():
|
|
173
|
+
inp = '[{"a":1},{"a":2},{"a":3}]'
|
|
174
|
+
out = dq('-i', 'json', '-o', 'jsonl', '-p', 'a', input_text=inp)
|
|
175
|
+
lines = [l for l in out.strip().split('\n') if l]
|
|
176
|
+
assert lines == ['1', '2', '3']
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Format conversion
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def test_json_to_jsonl():
|
|
184
|
+
inp = '[{"a":1},{"a":2}]'
|
|
185
|
+
out = dq('-i', 'json', '-o', 'jsonl', '-p', 'a', input_text=inp)
|
|
186
|
+
lines = out.strip().split('\n')
|
|
187
|
+
assert lines == ['1', '2']
|
|
188
|
+
|
|
189
|
+
def test_jsonl_to_json_single():
|
|
190
|
+
out = dq('-i', 'jsonl', '-o', 'json', '-p', 'a', input_text='{"a":1}\n')
|
|
191
|
+
assert json.loads(out) == 1
|
|
192
|
+
|
|
193
|
+
def test_jsonl_to_json_multi():
|
|
194
|
+
out = dq('-i', 'jsonl', '-o', 'json', '-p', 'a',
|
|
195
|
+
input_text='{"a":1}\n{"a":2}\n')
|
|
196
|
+
assert json.loads(out) == [1, 2]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# CSV
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def test_csv_get():
|
|
204
|
+
inp = 'name,age\nalice,30\nbob,25\n'
|
|
205
|
+
out = dq('-i', 'csv', '-o', 'jsonl', '-p', 'name', input_text=inp)
|
|
206
|
+
lines = [json.loads(l) for l in out.strip().split('\n') if l]
|
|
207
|
+
assert lines == ['alice', 'bob']
|
|
208
|
+
|
|
209
|
+
def test_csv_projection():
|
|
210
|
+
inp = 'name,age,city\nalice,30,nyc\n'
|
|
211
|
+
out = dq('-i', 'csv', '-o', 'jsonl', '-p', 'name', '-p', 'age', input_text=inp)
|
|
212
|
+
lines = [json.loads(l) for l in out.strip().split('\n') if l]
|
|
213
|
+
assert lines == [{"name": "alice", "age": "30"}]
|
|
214
|
+
|
|
215
|
+
def test_csv_output():
|
|
216
|
+
inp = '[{"name":"alice","age":30},{"name":"bob","age":25}]'
|
|
217
|
+
out = dq('-i', 'json', '-o', 'csv', input_text=inp)
|
|
218
|
+
lines = out.strip().split('\n')
|
|
219
|
+
assert lines[0] == 'name,age'
|
|
220
|
+
assert 'alice' in lines[1]
|
|
221
|
+
assert 'bob' in lines[2]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# Path files
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
def test_get_path_file():
|
|
229
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
|
230
|
+
f.write('a\nb\n')
|
|
231
|
+
f.flush()
|
|
232
|
+
try:
|
|
233
|
+
out = dq('-pf', f.name, input_text='{"a": 1, "b": 2, "c": 3}')
|
|
234
|
+
assert json.loads(out) == {"a": 1, "b": 2}
|
|
235
|
+
finally:
|
|
236
|
+
os.unlink(f.name)
|
|
237
|
+
|
|
238
|
+
def test_update_path_file():
|
|
239
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
|
240
|
+
f.write('a 42\nb 43\n')
|
|
241
|
+
f.flush()
|
|
242
|
+
try:
|
|
243
|
+
out = dq('update', '-pf', f.name,
|
|
244
|
+
input_text='{"a": 1, "b": 2, "c": 3}')
|
|
245
|
+
assert json.loads(out) == {"a": 42, "b": 43, "c": 3}
|
|
246
|
+
finally:
|
|
247
|
+
os.unlink(f.name)
|
|
248
|
+
|
|
249
|
+
def test_path_file_comments_and_blanks():
|
|
250
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
|
251
|
+
f.write('# comment\na\n\nb\n# another comment\n')
|
|
252
|
+
f.flush()
|
|
253
|
+
try:
|
|
254
|
+
out = dq('-pf', f.name, input_text='{"a": 1, "b": 2, "c": 3}')
|
|
255
|
+
assert json.loads(out) == {"a": 1, "b": 2}
|
|
256
|
+
finally:
|
|
257
|
+
os.unlink(f.name)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# Error handling
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
def test_no_path_passthrough():
|
|
265
|
+
out = dq(input_text='{"a": 1}')
|
|
266
|
+
assert json.loads(out) == {"a": 1}
|
|
267
|
+
|
|
268
|
+
def test_update_no_path():
|
|
269
|
+
stderr = dq('update', input_text='{"a": 1}', expect_fail=True)
|
|
270
|
+
assert 'path' in stderr.lower() or 'requires' in stderr.lower()
|
|
271
|
+
|
|
272
|
+
def test_remove_no_path():
|
|
273
|
+
stderr = dq('remove', input_text='{"a": 1}', expect_fail=True)
|
|
274
|
+
assert 'path' in stderr.lower() or 'requires' in stderr.lower()
|
|
275
|
+
|
|
276
|
+
def test_update_missing_value():
|
|
277
|
+
stderr = dq('update', '-p', 'a', input_text='{"a": 1}', expect_fail=True)
|
|
278
|
+
assert stderr # should have an error message
|
|
279
|
+
|
|
280
|
+
def test_invalid_path():
|
|
281
|
+
stderr = dq('-p', '[[[invalid', input_text='{"a": 1}', expect_fail=True)
|
|
282
|
+
assert stderr
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
# YAML (optional)
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
def test_json_to_yaml():
|
|
290
|
+
pytest.importorskip('yaml')
|
|
291
|
+
out = dq('-i', 'json', '-o', 'yaml', input_text='{"a": 1, "b": 2}')
|
|
292
|
+
import yaml
|
|
293
|
+
assert yaml.safe_load(out) == {"a": 1, "b": 2}
|
|
294
|
+
|
|
295
|
+
def test_yaml_to_json():
|
|
296
|
+
pytest.importorskip('yaml')
|
|
297
|
+
out = dq('-i', 'yaml', '-o', 'json', input_text='a: 1\nb: 2\n')
|
|
298
|
+
assert json.loads(out) == {"a": 1, "b": 2}
|
|
299
|
+
|
|
300
|
+
def test_yaml_multidoc():
|
|
301
|
+
pytest.importorskip('yaml')
|
|
302
|
+
inp = 'a: 1\n---\na: 2\n'
|
|
303
|
+
out = dq('-i', 'yaml', '-o', 'jsonl', '-p', 'a', input_text=inp)
|
|
304
|
+
lines = [l for l in out.strip().split('\n') if l]
|
|
305
|
+
assert lines == ['1', '2']
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
# TOML (optional)
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
def test_toml_get():
|
|
313
|
+
pytest.importorskip('tomllib')
|
|
314
|
+
inp = 'a = 1\nb = 2\n'
|
|
315
|
+
out = dq('-i', 'toml', '-o', 'json', '-p', 'a', input_text=inp)
|
|
316
|
+
assert out.strip() == '1'
|
|
317
|
+
|
|
318
|
+
def test_toml_projection():
|
|
319
|
+
pytest.importorskip('tomllib')
|
|
320
|
+
inp = 'a = 1\nb = 2\nc = 3\n'
|
|
321
|
+
out = dq('-i', 'toml', '-o', 'json', '-p', 'a', '-p', 'b', input_text=inp)
|
|
322
|
+
assert json.loads(out) == {"a": 1, "b": 2}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
# Python literals
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
def test_py_read_dict():
|
|
330
|
+
out = dq('-i', 'py', '-o', 'json', '-p', 'a',
|
|
331
|
+
input_text="{'a': 1, 'b': 2}")
|
|
332
|
+
assert out.strip() == '1'
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_py_read_none():
|
|
336
|
+
out = dq('-i', 'py', '-o', 'json', input_text="{'a': None}")
|
|
337
|
+
assert json.loads(out) == {"a": None}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_py_read_list_streams():
|
|
341
|
+
out = dq('-i', 'py', '-o', 'jsonl', '-p', 'a',
|
|
342
|
+
input_text="[{'a': 1}, {'a': 2}]")
|
|
343
|
+
lines = [l for l in out.strip().split('\n') if l]
|
|
344
|
+
assert lines == ['1', '2']
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_py_write_single():
|
|
348
|
+
out = dq('-i', 'json', '-o', 'py', input_text='{"a": null, "b": true}')
|
|
349
|
+
import ast
|
|
350
|
+
assert ast.literal_eval(out.strip()) == {"a": None, "b": True}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_py_write_multi():
|
|
354
|
+
out = dq('-i', 'json', '-o', 'py', input_text='[{"a": 1}, {"a": 2}]')
|
|
355
|
+
import ast
|
|
356
|
+
assert ast.literal_eval(out.strip()) == [{"a": 1}, {"a": 2}]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_pyl_read():
|
|
360
|
+
inp = "{'a': 1}\n{'a': 2}\n"
|
|
361
|
+
out = dq('-i', 'pyl', '-o', 'jsonl', '-p', 'a', input_text=inp)
|
|
362
|
+
lines = [l for l in out.strip().split('\n') if l]
|
|
363
|
+
assert lines == ['1', '2']
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def test_pyl_read_multiline():
|
|
367
|
+
inp = "{\n 'a': 1,\n 'b': 2,\n}\n{\n 'a': 3,\n 'b': 4,\n}\n"
|
|
368
|
+
out = dq('-i', 'pyl', '-o', 'jsonl', '-p', 'a', input_text=inp)
|
|
369
|
+
lines = [l for l in out.strip().split('\n') if l]
|
|
370
|
+
assert lines == ['1', '3']
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def test_pyl_write():
|
|
374
|
+
inp = '{"a":1}\n{"a":2}\n'
|
|
375
|
+
out = dq('-i', 'jsonl', '-o', 'pyl', input_text=inp)
|
|
376
|
+
import ast
|
|
377
|
+
lines = [ast.literal_eval(l) for l in out.strip().split('\n') if l]
|
|
378
|
+
assert lines == [{"a": 1}, {"a": 2}]
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_py_roundtrip():
|
|
382
|
+
inp = "{'x': None, 'y': (1, 2), 'z': True}"
|
|
383
|
+
out = dq('-i', 'py', '-o', 'py', input_text=inp)
|
|
384
|
+
import ast
|
|
385
|
+
assert ast.literal_eval(out.strip()) == {'x': None, 'y': (1, 2), 'z': True}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_py_multidoc():
|
|
389
|
+
inp = "{\n 'a': 1,\n}\n{\n 'a': 2,\n}\n"
|
|
390
|
+
out = dq('-i', 'py', '-o', 'jsonl', '-p', 'a', input_text=inp)
|
|
391
|
+
lines = [l for l in out.strip().split('\n') if l]
|
|
392
|
+
assert lines == ['1', '2']
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def test_py_multidoc_get():
|
|
396
|
+
inp = "{'a': 1, 'b': 2}\n{'a': 3, 'b': 4}\n"
|
|
397
|
+
out = dq('-i', 'py', '-o', 'pyl', input_text=inp)
|
|
398
|
+
import ast
|
|
399
|
+
lines = [ast.literal_eval(l) for l in out.strip().split('\n') if l]
|
|
400
|
+
assert lines == [{'a': 1, 'b': 2}, {'a': 3, 'b': 4}]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ---------------------------------------------------------------------------
|
|
404
|
+
# Pack / Unpack
|
|
405
|
+
# ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
def test_unpack_passthrough():
|
|
408
|
+
out = dq('--unpack', input_text='{"a": {"b": 1}, "c": 2}')
|
|
409
|
+
assert json.loads(out) == {"a.b": 1, "c": 2}
|
|
410
|
+
|
|
411
|
+
def test_unpack_update():
|
|
412
|
+
out = dq('--unpack', 'update', '-p', 'a.b', '99',
|
|
413
|
+
input_text='{"a": {"b": 1}, "c": 2}')
|
|
414
|
+
assert json.loads(out) == {"a.b": 99, "c": 2}
|
|
415
|
+
|
|
416
|
+
def test_unpack_remove():
|
|
417
|
+
out = dq('--unpack', 'remove', '-p', 'a',
|
|
418
|
+
input_text='{"a": 1, "b": {"c": 2}}')
|
|
419
|
+
assert json.loads(out) == {"b.c": 2}
|
|
420
|
+
|
|
421
|
+
def test_pack_passthrough():
|
|
422
|
+
out = dq('--pack', input_text='{"a.b": 1, "c": 2}')
|
|
423
|
+
assert json.loads(out) == {"a": {"b": 1}, "c": 2}
|
|
424
|
+
|
|
425
|
+
def test_pack_then_get():
|
|
426
|
+
out = dq('--pack', '-p', 'a.b',
|
|
427
|
+
input_text='{"a.b": 1, "c": 2}')
|
|
428
|
+
assert out.strip() == '1'
|
|
429
|
+
|
|
430
|
+
def test_pack_unpack_roundtrip():
|
|
431
|
+
inp = '{"a": {"b": [1, 2]}, "c": 3}'
|
|
432
|
+
unpacked = dq('--unpack', input_text=inp)
|
|
433
|
+
repacked = dq('--pack', input_text=unpacked)
|
|
434
|
+
assert json.loads(repacked) == json.loads(inp)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
# File input
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
def test_file_input():
|
|
442
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
443
|
+
f.write('{"a": 1, "b": 2}')
|
|
444
|
+
f.flush()
|
|
445
|
+
try:
|
|
446
|
+
out = dq('-f', f.name, '-p', 'a')
|
|
447
|
+
assert out.strip() == '1'
|
|
448
|
+
finally:
|
|
449
|
+
os.unlink(f.name)
|
|
450
|
+
|
|
451
|
+
def test_file_auto_detect_yaml():
|
|
452
|
+
pytest.importorskip('yaml')
|
|
453
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
454
|
+
f.write('a: 1\nb: 2\n')
|
|
455
|
+
f.flush()
|
|
456
|
+
try:
|
|
457
|
+
out = dq('-f', f.name, '-o', 'json', '-p', 'a')
|
|
458
|
+
assert out.strip() == '1'
|
|
459
|
+
finally:
|
|
460
|
+
os.unlink(f.name)
|
|
461
|
+
|
|
462
|
+
def test_file_auto_detect_csv():
|
|
463
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f:
|
|
464
|
+
f.write('name,age\nalice,30\n')
|
|
465
|
+
f.flush()
|
|
466
|
+
try:
|
|
467
|
+
out = dq('-f', f.name, '-o', 'jsonl', '-p', 'name')
|
|
468
|
+
assert json.loads(out.strip()) == 'alice'
|
|
469
|
+
finally:
|
|
470
|
+
os.unlink(f.name)
|
|
471
|
+
|
|
472
|
+
def test_file_explicit_format_overrides():
|
|
473
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
|
474
|
+
f.write('{"a": 1}')
|
|
475
|
+
f.flush()
|
|
476
|
+
try:
|
|
477
|
+
out = dq('-f', f.name, '-i', 'json', '-p', 'a')
|
|
478
|
+
assert out.strip() == '1'
|
|
479
|
+
finally:
|
|
480
|
+
os.unlink(f.name)
|
|
481
|
+
|
|
482
|
+
def test_file_not_found():
|
|
483
|
+
stderr = dq('-f', '/tmp/nonexistent_dq_test.json', '-p', 'a',
|
|
484
|
+
input_text='', expect_fail=True)
|
|
485
|
+
assert stderr
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
# Version
|
|
490
|
+
# ---------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
def test_version_flag():
|
|
493
|
+
result = dq_subprocess('--version')
|
|
494
|
+
assert result.returncode == 0
|
|
495
|
+
assert 'dq' in result.stdout
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
# ---------------------------------------------------------------------------
|
|
499
|
+
# Subprocess smoke tests
|
|
500
|
+
# ---------------------------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
def test_subprocess_get():
|
|
503
|
+
result = dq_subprocess('-p', 'a', input='{"a": 1, "b": 2}')
|
|
504
|
+
assert result.returncode == 0
|
|
505
|
+
assert result.stdout.strip() == '1'
|
|
506
|
+
|
|
507
|
+
def test_subprocess_update():
|
|
508
|
+
result = dq_subprocess('update', '-p', 'a', '42', input='{"a": 1}')
|
|
509
|
+
assert result.returncode == 0
|
|
510
|
+
assert json.loads(result.stdout) == {"a": 42}
|
|
511
|
+
|
|
512
|
+
def test_subprocess_error():
|
|
513
|
+
result = dq_subprocess('update', input='{"a": 1}')
|
|
514
|
+
assert result.returncode != 0
|
|
515
|
+
assert result.stderr
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for JSON-style sentinel aliases: true/false/null alongside True/False/None.
|
|
3
|
+
"""
|
|
4
|
+
import dotted
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_parse_true_alias():
|
|
8
|
+
p = dotted.parse('active=true')
|
|
9
|
+
assert dotted.assemble(p) == 'active=True'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_parse_false_alias():
|
|
13
|
+
p = dotted.parse('enabled=false')
|
|
14
|
+
assert dotted.assemble(p) == 'enabled=False'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_parse_null_alias():
|
|
18
|
+
p = dotted.parse('x=null')
|
|
19
|
+
assert dotted.assemble(p) == 'x=None'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_value_guard_true():
|
|
23
|
+
assert dotted.get({'x': True}, 'x=true') is True
|
|
24
|
+
assert dotted.get({'x': False}, 'x=true') is None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_value_guard_false():
|
|
28
|
+
assert dotted.get({'x': False}, 'x=false') is False
|
|
29
|
+
assert dotted.get({'x': True}, 'x=false') is None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_value_guard_null():
|
|
33
|
+
assert dotted.get({'x': None}, 'x=null') is None
|
|
34
|
+
assert dotted.get({'x': 0}, 'x=null') is None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_filter_true_alias():
|
|
38
|
+
data = [{'active': True}, {'active': False}]
|
|
39
|
+
assert dotted.get(data, '[active=true]') == [{'active': True}]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_filter_false_alias():
|
|
43
|
+
data = [{'active': True}, {'active': False}]
|
|
44
|
+
assert dotted.get(data, '[active=false]') == [{'active': False}]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_filter_null_alias():
|
|
48
|
+
data = [{'x': 1}, {'x': None}]
|
|
49
|
+
assert dotted.get(data, '[x=null]') == [{'x': None}]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_aliases_equivalent_to_canonical():
|
|
53
|
+
data = [{'a': True, 'b': False, 'c': None}]
|
|
54
|
+
assert dotted.get(data, '[a=true]') == dotted.get(data, '[a=True]')
|
|
55
|
+
assert dotted.get(data, '[b=false]') == dotted.get(data, '[b=False]')
|
|
56
|
+
assert dotted.get(data, '[c=null]') == dotted.get(data, '[c=None]')
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_neq_aliases():
|
|
60
|
+
data = [{'a': True}, {'a': False}, {'a': None}]
|
|
61
|
+
assert dotted.get(data, '[a!=true]') == [{'a': False}, {'a': None}]
|
|
62
|
+
assert dotted.get(data, '[a!=null]') == [{'a': True}, {'a': False}]
|