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.
Files changed (82) hide show
  1. {dotted_notation-0.43.1/dotted_notation.egg-info → dotted_notation-0.43.2}/PKG-INFO +1 -1
  2. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/grammar.py +3 -3
  3. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/matchers.py +1 -1
  4. {dotted_notation-0.43.1 → dotted_notation-0.43.2/dotted_notation.egg-info}/PKG-INFO +1 -1
  5. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted_notation.egg-info/SOURCES.txt +1 -0
  6. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/setup.py +1 -1
  7. dotted_notation-0.43.2/tests/test_cli.py +515 -0
  8. dotted_notation-0.43.2/tests/test_json_sentinels.py +62 -0
  9. dotted_notation-0.43.2/tests/test_strict.py +314 -0
  10. dotted_notation-0.43.2/tests/test_value_guard.py +280 -0
  11. dotted_notation-0.43.1/tests/test_cli.py +0 -551
  12. dotted_notation-0.43.1/tests/test_strict.py +0 -323
  13. dotted_notation-0.43.1/tests/test_value_guard.py +0 -285
  14. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/LICENSE +0 -0
  15. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/README.md +0 -0
  16. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/__init__.py +0 -0
  17. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/__main__.py +0 -0
  18. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/access.py +0 -0
  19. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/api.py +0 -0
  20. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/base.py +0 -0
  21. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/cli/__init__.py +0 -0
  22. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/cli/_compat.py +0 -0
  23. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/cli/formats.py +0 -0
  24. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/cli/main.py +0 -0
  25. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/containers.py +0 -0
  26. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/engine.py +0 -0
  27. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/filters.py +0 -0
  28. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/groups.py +0 -0
  29. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/predicates.py +0 -0
  30. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/recursive.py +0 -0
  31. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/results.py +0 -0
  32. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/sqlize.py +0 -0
  33. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/transforms.py +0 -0
  34. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/utils.py +0 -0
  35. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/utypes.py +0 -0
  36. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted/wrappers.py +0 -0
  37. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted_notation.egg-info/dependency_links.txt +0 -0
  38. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted_notation.egg-info/entry_points.txt +0 -0
  39. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted_notation.egg-info/requires.txt +0 -0
  40. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/dotted_notation.egg-info/top_level.txt +0 -0
  41. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/setup.cfg +0 -0
  42. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/__init__.py +0 -0
  43. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_api.py +0 -0
  44. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_appender.py +0 -0
  45. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_assemble.py +0 -0
  46. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_attrs.py +0 -0
  47. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_bindings.py +0 -0
  48. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_concat.py +0 -0
  49. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_container_filter.py +0 -0
  50. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_cut.py +0 -0
  51. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_empty.py +0 -0
  52. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_filter_keyvalue.py +0 -0
  53. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_get.py +0 -0
  54. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_guard_transforms.py +0 -0
  55. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_invert.py +0 -0
  56. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_keys_values.py +0 -0
  57. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_match.py +0 -0
  58. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_matchable.py +0 -0
  59. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_named_subst.py +0 -0
  60. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_negation.py +0 -0
  61. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_nop.py +0 -0
  62. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_numeric.py +0 -0
  63. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_opgroup.py +0 -0
  64. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_pluck.py +0 -0
  65. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_predicates.py +0 -0
  66. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_quote_idempotent.py +0 -0
  67. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_recursive.py +0 -0
  68. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_reference.py +0 -0
  69. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_replace.py +0 -0
  70. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_slice.py +0 -0
  71. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_softcut.py +0 -0
  72. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_sqlize.py +0 -0
  73. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_string_glob.py +0 -0
  74. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_subst_escape.py +0 -0
  75. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_subst_transforms.py +0 -0
  76. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_threading.py +0 -0
  77. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_transforms.py +0 -0
  78. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_translate.py +0 -0
  79. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_type_restriction.py +0 -0
  80. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_unpack.py +0 -0
  81. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_update.py +0 -0
  82. {dotted_notation-0.43.1 → dotted_notation-0.43.2}/tests/test_update_if.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.43.1
3
+ Version: 0.43.2
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
@@ -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)
@@ -122,7 +122,7 @@ class Boolean(Const):
122
122
  """
123
123
  @property
124
124
  def value(self):
125
- return self.args[0] == 'True'
125
+ return self.args[0].lower() == 'true'
126
126
  def __repr__(self):
127
127
  return str(self.value)
128
128
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.43.1
3
+ Version: 0.43.2
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
@@ -45,6 +45,7 @@ tests/test_filter_keyvalue.py
45
45
  tests/test_get.py
46
46
  tests/test_guard_transforms.py
47
47
  tests/test_invert.py
48
+ tests/test_json_sentinels.py
48
49
  tests/test_keys_values.py
49
50
  tests/test_match.py
50
51
  tests/test_matchable.py
@@ -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.1",
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}]