Coverage for jsonurl_test.py: 100%
308 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-06 11:15 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-06 11:15 +0000
1import json
2import string
4import pytest
6import jsonurl_py as jsonurl
7from conftest import (
8 assert_load,
9 assert_load_fail,
10 assert_roundtrip,
11 assert_roundtrip_data,
12)
15def test_dumps():
16 assert jsonurl.dumps(dict(a=1)) == "(a:1)"
17 assert jsonurl.dumps(dict(a="b c")) == "(a:b+c)"
18 assert jsonurl.dumps(dict(a="b$c")) == "(a:b%24c)"
21def test_dump_empty_string():
22 assert "''" == jsonurl.dumps("")
23 assert "(a:'')" == jsonurl.dumps(dict(a=""))
26def test_percent_caps():
27 assert_load("ll", "%6c%6C")
28 assert_load("jklmno", r"%6A%6B%6C%6D%6E%6F")
31def test_percent_error():
32 assert_load_fail(r"%6")
33 assert_load_fail(r"%6c%")
34 assert_load_fail(r"%6c%6")
37def test_dump_escape_aqf():
38 assert "a!!" == jsonurl.dumps("a!", aqf=True)
41def test_dump_escape_nonaqf():
42 assert "a%21" == jsonurl.dumps("a!", aqf=False)
45def test_dump_null_aqf():
46 assert "!null" == jsonurl.dumps("null", aqf=True)
49def test_load_null_aqf():
50 assert "null" == jsonurl.loads("!null", aqf=True)
53def test_aqf_escape_once():
54 assert_load(["!", ""], "!!,!e", aqf=True, implied_list=True)
55 assert_load(["!", ""], "(!!,!e)", aqf=True, implied_list=False)
56 assert_load([",", ")"], "!,,!)", aqf=True, implied_list=True)
59def test_roundtrip_aqf_escapes():
60 assert_roundtrip("a!!", "a!", aqf=True)
61 assert_roundtrip("!!", "!", aqf=True)
64def test_roundtrip_aqf_escape_paren():
65 assert_roundtrip("!(", "(", aqf=True)
68def test_roundtrip_aqf_structural():
69 assert_roundtrip_data(
70 ["!", "+", "(", ")", ",", ":"],
71 aqf=True,
72 implied_list=True,
73 )
76def test_roundtrip_aqf_escape_many():
77 assert_roundtrip_data(
78 ["!", "a!", "!a", "!e", "e!", "!(", "", None, True, "true"],
79 aqf=True,
80 implied_list=True,
81 )
84def test_dump_empty_string_aqf():
85 assert_roundtrip("(!e:a)", {"": "a"}, aqf=True)
86 assert_roundtrip("(a:!e)", {"a": ""}, aqf=True)
87 assert_roundtrip("!e:a", {"": "a"}, aqf=True, implied_dict=True)
88 assert_roundtrip("a:!e", {"a": ""}, aqf=True, implied_dict=True)
91def test_dump_numlike_string():
92 assert "(a:'123')" == jsonurl.dumps(dict(a="123"))
93 assert "(a:'1e3')" == jsonurl.dumps(dict(a="1e3"))
94 assert "(a:'1e-3')" == jsonurl.dumps(dict(a="1e-3"))
95 assert "(a:%2B123)" == jsonurl.dumps(dict(a="+123"))
98def test_percent():
99 jsonurl._load_percent("%31", 0)[0] == chr(0x11)
100 jsonurl._load_percent("%41%42", 0)[0] == chr(0x41) + chr(0x42)
103def test_loads_atoms():
104 assert jsonurl.loads("aaa") == "aaa"
105 assert jsonurl.loads("123") == 123
106 assert jsonurl.loads("true") == True
107 assert jsonurl.loads("false") == False
108 assert jsonurl.loads("null") == None
111def test_loads_dict():
112 assert jsonurl.loads("(a:b)") == dict(a="b")
113 assert jsonurl.loads("(a:1)") == dict(a=1)
116def test_loads_dict_many():
117 assert jsonurl.loads("(a:1,b:2,c:3)") == dict(a=1, b=2, c=3)
120def test_loads_list():
121 assert jsonurl.loads("(a,b)") == ["a", "b"]
122 assert jsonurl.loads("(true,false,1,null)") == [True, False, 1, None]
123 assert jsonurl.loads("(a,(1,2),b)") == ["a", [1, 2], "b"]
126def test_empty_input():
127 with pytest.raises(jsonurl.ParseError):
128 jsonurl.loads("")
131def test_unenc():
132 assert "a~/*b" == jsonurl.loads("a~/*b")
135def test_empty_composite():
136 assert {} == jsonurl.loads("()")
139def test_one_item_list():
140 assert [1] == jsonurl.loads("(1)")
143def test_one_item_nested_list():
144 assert [[1]] == jsonurl.loads("((1))")
147def test_number():
148 assert 1.2 == jsonurl.loads("1.2")
149 assert -1e3 == jsonurl.loads("-1e3")
150 assert 3.2e-5 == jsonurl.loads("3.2e-5")
151 assert 3.2e5 == jsonurl.loads("3.2e+5")
154def test_dumps_float():
155 assert "1.2" == jsonurl.dumps(1.2)
156 assert "1000.0" == jsonurl.dumps(1e3)
157 assert "1000.0" == json.dumps(1e3)
160def test_error_on_plus_number():
161 with pytest.raises(Exception):
162 json.loads("+123")
163 assert jsonurl.loads("+123") == " 123"
166def test_nonumber():
167 assert "1" == jsonurl.loads("%31")
170def test_qstr():
171 assert "abc" == jsonurl.loads("'abc'")
174def test_load_quote_percent():
175 assert "'" == jsonurl.loads(r"%27")
176 assert "''" == jsonurl.loads(r"%27%27")
177 assert "'true'" == jsonurl.loads(r"%27true%27")
180def test_save_implied_list():
181 assert "a,'1'" == jsonurl.dumps(["a", "1"], implied_list=True)
184def test_load_implied_list():
185 assert ["a", "b"] == jsonurl.loads("a,b", implied_list=True)
186 assert ["a", {"b": "c"}] == jsonurl.loads("a,(b:c)", implied_list=True)
187 with pytest.raises(jsonurl.ParseError):
188 jsonurl.loads("a,b:c,", implied_list=True)
189 with pytest.raises(jsonurl.ParseError):
190 jsonurl.loads("a,,", implied_list=True)
193def test_save_implied_dict():
194 assert "a:'1',b:''" == jsonurl.dumps(dict(a="1", b=""), implied_dict=True)
197def test_load_implied_dict():
198 assert dict(a="1", b="") == jsonurl.loads("a:'1',b:''", implied_dict=True)
201def test_empty_implied_list_save():
202 assert "" == jsonurl.dumps([], implied_list=True)
205def test_empty_implied_dict_save():
206 assert "" == jsonurl.dumps({}, implied_dict=True)
209def test_empty_implied_list_load():
210 assert [] == jsonurl.loads("", implied_list=True)
213def test_empty_implied_dict_load():
214 assert {} == jsonurl.loads("", implied_dict=True)
217def test_unquote_aqf():
218 assert "true" == jsonurl._unquote_aqf("true")
219 assert "true" == jsonurl._unquote_aqf("!true")
220 assert "1e23" == jsonurl._unquote_aqf("1e!23")
221 with pytest.raises(jsonurl.ParseError):
222 jsonurl._unquote_aqf("1!e23")
223 assert "1e-23" == jsonurl._unquote_aqf("1e!-23")
224 assert "1e+23" == jsonurl._unquote_aqf("1e!+23")
225 assert "hi!ho?" == jsonurl._unquote_aqf("hi!!ho?")
226 with pytest.raises(jsonurl.ParseError):
227 jsonurl._unquote_aqf("hi!ho?")
230def test_bool_percent():
231 assert True == jsonurl.loads("true", aqf=False)
232 assert True == jsonurl.loads("true", aqf=True)
233 assert "true" == jsonurl.loads("%74rue", aqf=False)
234 assert "true" == jsonurl.loads("%74rue", aqf=True)
235 assert "!true" == jsonurl.loads("%21%74rue", aqf=False)
236 assert "!true" == jsonurl.loads("!%74rue", aqf=False)
237 assert "!true" == jsonurl.loads("%21true", aqf=False)
238 assert "!true" == jsonurl.loads("!true", aqf=False)
239 assert "true" == jsonurl.loads("%21%74rue", aqf=True)
240 assert "true" == jsonurl.loads("!%74rue", aqf=True)
241 assert "true" == jsonurl.loads("%21true", aqf=True)
242 assert "true" == jsonurl.loads("!true", aqf=True)
245def test_more_aqf():
246 assert_load("", "!e", aqf=True)
247 assert_load("true", "!true", aqf=True)
248 assert_load("n", "!n", aqf=True)
249 assert_load("n", "%21n", aqf=True)
250 assert_load("n", "!%6e", aqf=True)
251 assert_load(")", "!)", aqf=True)
252 assert_load("true", "%74rue", aqf=True)
253 assert_load("!true", "!!true", aqf=True)
254 assert_load("false", "!false", aqf=True)
255 assert_load(True, "true", aqf=True)
256 assert_load(False, "false", aqf=True)
257 assert_load("hi!", "hi!!", aqf=True)
258 assert_load("hi)", "hi!)", aqf=True)
261def test_structural_aqf():
262 assert_load("(", "!(", aqf=True)
263 assert_load("(", "!%28", aqf=True)
264 assert_load("(", "%21(", aqf=True)
265 assert_load("(", "%21%28", aqf=True)
266 with pytest.raises(jsonurl.ParseError):
267 jsonurl.loads("%28%21", aqf=True)
268 assert_load("(!", "%28%21", aqf=False)
269 assert_load("(", "%21(", aqf=True)
270 assert_load("z(", "%7a%21%28", aqf=True)
273def test_unterminated_qstr():
274 assert_load_fail("'ab")
275 assert_load_fail("a,'ab", implied_list=True)
278def test_percent_qstr():
279 assert_load("a'b", "a%27b")
280 assert_load("a'b", "'a%27b'")
281 assert_load("abc", "'ab%63'")
282 assert_load_fail("'ab%6'")
283 assert_load_fail("'ab%'")
286def test_aqf_escape_after_percent():
287 assert_load("true", "%74rue", aqf=True)
288 assert_load("trun", "%74ru!n", aqf=True)
291def test_aqf_e_invalid_escape():
292 assert_load_fail("a!eb", aqf=True)
293 assert_load_fail("a!e", aqf=True)
294 assert_load_fail("!ea", aqf=True)
297def test_plus_in_qstr():
298 assert_load("a b", "'a+b'")
301def test_space_in_qstr():
302 assert_load_fail("'a b'")
305def test_unterminated_dict():
306 assert_load_fail("(a:b,")
307 assert_load_fail("(a:b,c")
308 assert_load_fail("(a:b,c:")
309 assert_load_fail("(a:b,c:d")
312def test_unterminated_dict_implied():
313 assert_load_fail("a:b,", implied_dict=True)
314 assert_load_fail("a:b,c", implied_dict=True)
315 assert_load_fail("a:b,c:", implied_dict=True)
316 assert_load_fail("a:b,c,", implied_dict=True)
317 assert_load_fail("a:b,c:d:", implied_dict=True)
320def test_unencoded_ascii_digits():
321 text = string.ascii_letters + string.digits
322 assert text == jsonurl.dumps(text)
323 assert text == jsonurl.loads(text)
326def test_load_unencoded_special():
327 text = "-._~!$*/;?@"
328 assert text == jsonurl.loads(text)
331def test_dump_unencoded_special():
332 assert "%21%24%2A%2F%3B%3F%40" == jsonurl.dumps("!$*/;?@")
333 assert "-._~" == jsonurl.dumps("-._~")
334 assert "%24%2A%2F%3B%3F%40" == jsonurl.dumps("$*/;?@", aqf=True)
335 assert "%24%2A%2F%3B%3F%40%27" == jsonurl.dumps("$*/;?@'", aqf=True)
338def test_aqf_single_quote_safe():
339 assert "a%27b" == jsonurl.dumps("a'b", aqf=True)
340 assert "a'b" == jsonurl.dumps("a'b", safe="'", aqf=True)
341 assert "a'b" == jsonurl.loads("a'b", aqf=True)
344def test_noaqf_single_quote_safe():
345 with pytest.raises(ValueError):
346 jsonurl.dumps("a'b", safe="'")
347 assert "a%27b" == jsonurl.dumps("a'b", aqf=False)
350def test_bad_safe():
351 with pytest.raises(ValueError):
352 jsonurl.dumps("a^b", safe="^")
353 with pytest.raises(jsonurl.ParseError):
354 jsonurl.loads("a^b")
357def test_fail_load_brackets():
358 for char in r"[](){}":
359 assert_load_fail(char)
362def test_dict_with_list_as_first_value():
363 assert_load({"a": [1, 2]}, "(a:(1,2))")
366def test_nested_list():
367 assert_load([[1, 2], 3], "((1,2),3)")
368 assert_load([1, [2, 3]], "(1,(2,3))")
371def test_aqf_escape_colon():
372 assert_load(":", "!:", aqf=True)
375def test_aqf_escape_semicolon():
376 assert_load_fail("!;", aqf=True)
379def test_aqf_load_apos():
380 assert_load("'ab", "'ab", aqf=True)
381 assert_load("a'b", "a'b", aqf=True)
382 assert_load("ab'", "ab'", aqf=True)
385def test_notaqf_load_apos_mid_fail():
386 assert_load_fail("'ab", aqf=False)
387 assert_load("a'b", "a'b", aqf=False)
388 assert_load("ab'", "ab'", aqf=False)
391def test_dump_badvalue():
392 import datetime
394 d = datetime.datetime.now()
395 with pytest.raises(TypeError):
396 json.dumps(dict(d=d))
397 with pytest.raises(TypeError):
398 jsonurl.dumps(dict(d=d))
401def test_badargs():
402 with pytest.raises(ValueError):
403 jsonurl.dumps("aaa", jsonurl.LoadOpts(), aqf=True) # type: ignore
404 with pytest.raises(ValueError):
405 jsonurl.loads("aaa", jsonurl.LoadOpts(), aqf=True) # type: ignore
408def test_aqf_percent_structural():
409 assert_load(["a", "b"], r"%28a%2cb%29", aqf=True)
410 assert_load({"a": "b"}, r"%28a%3ab%29", aqf=True)
413def test_aqf_ampersand():
414 assert_load("a&b", r"a%26b", aqf=True)
415 assert_load_fail(r"a&b", aqf=True)
416 assert_load("a=b", r"a%3db", aqf=True)
417 assert_load_fail(r"a=b", aqf=True)
420def test_unterminated_percent_message():
421 with pytest.raises(jsonurl.ParseError, match="Unterminated percent at pos 2"):
422 jsonurl.loads("ab%a")
423 with pytest.raises(jsonurl.ParseError, match="Unterminated percent at pos 1"):
424 jsonurl.loads("a%a", aqf=True)
427def test_invalid_bang_escape_message():
428 with pytest.raises(jsonurl.ParseError, match="Invalid !-escaped char 0x61"):
429 jsonurl.loads("!a", aqf=True)
432ERROR_STRINGS = [
433 "(",
434 ")",
435 "{",
436 "}",
437 ",",
438 ":",
439 "(1",
440 "(1,",
441 "(a:",
442 "(a:b",
443 "1,",
444 "()a",
445 "(1)a",
446 "(|",
447 "((1)",
448 "(1(",
449 "((1,2,)",
450 "(1,1",
451 "(1,a,()",
452 "(((1,1(",
453 "(((1))",
454 "((a:b)",
455 "(a:b,'')",
456 "((a:b(",
457 "(a:b,c)",
458 "(a:b,c:)",
459 "(a:b,c:,)",
460 "(a&b)",
461 "(a=b)",
462 "'a=b'",
463 "'a&b'",
464 "(a:)",
465 "(:a)",
466 "(a,,c)",
467]
470@pytest.mark.parametrize("arg", ERROR_STRINGS)
471def test_errors_strings(arg: str):
472 with pytest.raises(jsonurl.ParseError):
473 jsonurl.loads(arg)
476PARSE_DATA = [
477 ["()", {}],
478 [
479 "(true:true,false:false,null:null,empty:(),single:(0),nested:((1)),many:(-1,2.0,3e1,4e-2,5e0))",
480 {
481 True: True,
482 False: False,
483 None: None,
484 "empty": {},
485 "single": [0],
486 "nested": [[1]],
487 "many": [-1, 2.0, 3e1, 4e-2, 5],
488 },
489 ],
490 ["(1)", [1]],
491 ["(1,(2))", [1, [2]]],
492 ["(1,(a:2),3)", [1, {"a": 2}, 3]],
493 [
494 "(age:64,name:(first:Fred))",
495 {
496 "age": 64,
497 "name": {"first": "Fred"},
498 },
499 ],
500 ["(null,null)", [None, None]],
501 ["(a:b,c:d,e:f)", {"a": "b", "c": "d", "e": "f"}],
502 ["Bob's+house", "Bob's house"],
503 ["(%26true)", ["&true"]],
504 ["((%26true))", [["&true"]]],
505]
508@pytest.mark.parametrize("arg_out", PARSE_DATA)
509def test_parse_data(arg_out):
510 arg, out = arg_out
511 assert jsonurl.loads(arg) == out
514def test_dump_safe():
515 assert jsonurl.dumps("a'b", aqf=True) == "a%27b"
516 assert jsonurl.dumps("a^b", aqf=True) == "a%5Eb"
517 assert jsonurl.dumps("a/b", aqf=True) == "a%2Fb"
518 assert jsonurl.dumps("a@b", aqf=True) == "a%40b"
519 assert jsonurl.dumps("a/b", aqf=True, safe="/@") == "a/b"
520 assert jsonurl.dumps("a@b", aqf=True, safe="@/") == "a@b"
521 assert jsonurl.dumps("a/b", aqf=False, safe="/@") == "a/b"
522 assert jsonurl.dumps("a@b", aqf=False, safe="@/") == "a@b"
523 assert jsonurl.dumps("a-b", aqf=True) == "a-b"
524 assert jsonurl.dumps("a}{b", aqf=True) == "a%7D%7Bb"
525 assert jsonurl.dumps("a,b", aqf=True) == "a!,b"
528def test_distinguish_empty():
529 assert jsonurl.loads("(:)", distinguish_empty_list_dict=True) == {}
530 assert jsonurl.loads("()", distinguish_empty_list_dict=True) == []
531 assert jsonurl.loads("()", distinguish_empty_list_dict=False) == {}
532 with pytest.raises(jsonurl.ParseError):
533 assert jsonurl.loads("(:)", distinguish_empty_list_dict=False)
536def test_distinguish_error():
537 assert_load_fail("a:(:x)", distinguish_empty_list_dict=True, implied_dict=True)
538 assert_load_fail("a:(:", distinguish_empty_list_dict=True, implied_dict=True)
539 assert_load_fail("a:(", distinguish_empty_list_dict=True, implied_dict=True)
540 assert_load_fail("a:(x", distinguish_empty_list_dict=True, implied_dict=True)
543def test_distinguish_empty_complex():
544 assert_roundtrip(
545 "a:(:),b:(),c:null",
546 dict(a={}, b=[], c=None),
547 distinguish_empty_list_dict=True,
548 implied_dict=True,
549 )