edq-utils 0.1.9__py3-none-any.whl
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.
- edq/__init__.py +5 -0
- edq/cli/__init__.py +0 -0
- edq/cli/config/__init__.py +3 -0
- edq/cli/config/list.py +69 -0
- edq/cli/http/__init__.py +3 -0
- edq/cli/http/exchange-server.py +71 -0
- edq/cli/http/send-exchange.py +45 -0
- edq/cli/http/verify-exchanges.py +38 -0
- edq/cli/testing/__init__.py +3 -0
- edq/cli/testing/cli-test.py +49 -0
- edq/cli/version.py +28 -0
- edq/core/__init__.py +0 -0
- edq/core/argparser.py +137 -0
- edq/core/argparser_test.py +124 -0
- edq/core/config.py +268 -0
- edq/core/config_test.py +1038 -0
- edq/core/log.py +101 -0
- edq/core/version.py +6 -0
- edq/procedure/__init__.py +0 -0
- edq/procedure/verify_exchanges.py +85 -0
- edq/py.typed +0 -0
- edq/testing/__init__.py +3 -0
- edq/testing/asserts.py +65 -0
- edq/testing/cli.py +360 -0
- edq/testing/cli_test.py +15 -0
- edq/testing/httpserver.py +578 -0
- edq/testing/httpserver_test.py +424 -0
- edq/testing/run.py +142 -0
- edq/testing/testdata/cli/data/configs/empty/edq-config.json +1 -0
- edq/testing/testdata/cli/data/configs/simple-1/edq-config.json +4 -0
- edq/testing/testdata/cli/data/configs/simple-2/edq-config.json +3 -0
- edq/testing/testdata/cli/data/configs/value-number/edq-config.json +3 -0
- edq/testing/testdata/cli/tests/config/list/config_list_base.txt +16 -0
- edq/testing/testdata/cli/tests/config/list/config_list_config_value_number.txt +10 -0
- edq/testing/testdata/cli/tests/config/list/config_list_ignore_config.txt +14 -0
- edq/testing/testdata/cli/tests/config/list/config_list_no_config.txt +8 -0
- edq/testing/testdata/cli/tests/config/list/config_list_show_origin.txt +13 -0
- edq/testing/testdata/cli/tests/config/list/config_list_skip_header.txt +10 -0
- edq/testing/testdata/cli/tests/help_base.txt +9 -0
- edq/testing/testdata/cli/tests/platform_skip.txt +5 -0
- edq/testing/testdata/cli/tests/version_base.txt +6 -0
- edq/testing/testdata/http/exchanges/simple.httpex.json +5 -0
- edq/testing/testdata/http/exchanges/simple_anchor.httpex.json +5 -0
- edq/testing/testdata/http/exchanges/simple_file.httpex.json +10 -0
- edq/testing/testdata/http/exchanges/simple_file_binary.httpex.json +10 -0
- edq/testing/testdata/http/exchanges/simple_file_get_params.httpex.json +14 -0
- edq/testing/testdata/http/exchanges/simple_file_multiple.httpex.json +13 -0
- edq/testing/testdata/http/exchanges/simple_file_name.httpex.json +11 -0
- edq/testing/testdata/http/exchanges/simple_file_post_multiple.httpex.json +13 -0
- edq/testing/testdata/http/exchanges/simple_file_post_params.httpex.json +14 -0
- edq/testing/testdata/http/exchanges/simple_headers.httpex.json +8 -0
- edq/testing/testdata/http/exchanges/simple_jsonresponse_dict.httpex.json +7 -0
- edq/testing/testdata/http/exchanges/simple_jsonresponse_list.httpex.json +9 -0
- edq/testing/testdata/http/exchanges/simple_params.httpex.json +9 -0
- edq/testing/testdata/http/exchanges/simple_post.httpex.json +5 -0
- edq/testing/testdata/http/exchanges/simple_post_params.httpex.json +9 -0
- edq/testing/testdata/http/exchanges/simple_post_urlparams.httpex.json +5 -0
- edq/testing/testdata/http/exchanges/simple_urlparams.httpex.json +5 -0
- edq/testing/testdata/http/exchanges/specialcase_listparams_explicit.httpex.json +8 -0
- edq/testing/testdata/http/exchanges/specialcase_listparams_url.httpex.json +5 -0
- edq/testing/testdata/http/files/a.txt +1 -0
- edq/testing/testdata/http/files/tiny.png +0 -0
- edq/testing/unittest.py +88 -0
- edq/util/__init__.py +3 -0
- edq/util/dirent.py +340 -0
- edq/util/dirent_test.py +979 -0
- edq/util/encoding.py +18 -0
- edq/util/hash.py +41 -0
- edq/util/hash_test.py +89 -0
- edq/util/json.py +180 -0
- edq/util/json_test.py +228 -0
- edq/util/net.py +1008 -0
- edq/util/parse.py +33 -0
- edq/util/pyimport.py +94 -0
- edq/util/pyimport_test.py +119 -0
- edq/util/reflection.py +32 -0
- edq/util/time.py +75 -0
- edq/util/time_test.py +107 -0
- edq_utils-0.1.9.dist-info/METADATA +164 -0
- edq_utils-0.1.9.dist-info/RECORD +83 -0
- edq_utils-0.1.9.dist-info/WHEEL +5 -0
- edq_utils-0.1.9.dist-info/licenses/LICENSE +21 -0
- edq_utils-0.1.9.dist-info/top_level.txt +1 -0
edq/util/encoding.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import edq.util.dirent
|
|
5
|
+
|
|
6
|
+
def to_base64(data: typing.Union[bytes, str], encoding: str = edq.util.dirent.DEFAULT_ENCODING) -> str:
|
|
7
|
+
""" Convert a payload to a base64-encoded string. """
|
|
8
|
+
|
|
9
|
+
if (isinstance(data, str)):
|
|
10
|
+
data = data.encode(encoding)
|
|
11
|
+
|
|
12
|
+
data = base64.standard_b64encode(data)
|
|
13
|
+
return data.decode(encoding)
|
|
14
|
+
|
|
15
|
+
def from_base64(data: str, encoding: str = edq.util.dirent.DEFAULT_ENCODING) -> bytes:
|
|
16
|
+
""" Convert a base64-encoded string to bytes. """
|
|
17
|
+
|
|
18
|
+
return base64.b64decode(data.encode(encoding), validate = True)
|
edq/util/hash.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import edq.util.dirent
|
|
5
|
+
|
|
6
|
+
DEFAULT_CLIP_HASH_LENGTH: int = 8
|
|
7
|
+
|
|
8
|
+
def sha256_hex(payload: typing.Any, encoding: str = edq.util.dirent.DEFAULT_ENCODING) -> str:
|
|
9
|
+
""" Compute and return the hex string of the SHA3-256 encoding of the payload. """
|
|
10
|
+
|
|
11
|
+
if (isinstance(payload, str)):
|
|
12
|
+
payload = payload.encode(encoding)
|
|
13
|
+
|
|
14
|
+
digest = hashlib.new('sha256')
|
|
15
|
+
digest.update(payload)
|
|
16
|
+
return digest.hexdigest()
|
|
17
|
+
|
|
18
|
+
def clip_text(text: str, max_length: int, hash_length: int = DEFAULT_CLIP_HASH_LENGTH) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Return a clipped version of the input text that is no longer than the specified length.
|
|
21
|
+
If the base text is found to be too long,
|
|
22
|
+
then enough if the tail of the text will be removed to insert a note about the clipping
|
|
23
|
+
and the first |hash_length| characters of the hash from sha256_hex().
|
|
24
|
+
|
|
25
|
+
Note that the max length is actually a soft cap.
|
|
26
|
+
Longer strings can be generated if the original text is shorter than the notification
|
|
27
|
+
that will be inserted into the clipped text.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
if (len(text) <= max_length):
|
|
31
|
+
return text
|
|
32
|
+
|
|
33
|
+
hash_hex = sha256_hex(text)
|
|
34
|
+
notification = f"[text clipped {hash_hex[0:hash_length]}]"
|
|
35
|
+
|
|
36
|
+
# Don't clip the text if the final string would be longer.
|
|
37
|
+
if (len(notification) >= len(text)):
|
|
38
|
+
return text
|
|
39
|
+
|
|
40
|
+
keep_length = max(0, max_length - len(notification))
|
|
41
|
+
return text[0:keep_length] + notification
|
edq/util/hash_test.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import edq.testing.unittest
|
|
2
|
+
import edq.util.hash
|
|
3
|
+
|
|
4
|
+
class TestHash(edq.testing.unittest.BaseTest):
|
|
5
|
+
""" Test hash-based operations. """
|
|
6
|
+
|
|
7
|
+
def test_sha256_hex_base(self):
|
|
8
|
+
""" Test the base sha256 hash. """
|
|
9
|
+
|
|
10
|
+
# [(input, expected), ...]
|
|
11
|
+
test_cases = [
|
|
12
|
+
('foo', '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'),
|
|
13
|
+
('abcdefghijklmnopqrstuvwxyz1234567890', '77d721c817f9d216c1fb783bcad9cdc20aaa2427402683f1f75dd6dfbe657470'),
|
|
14
|
+
('', 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
for (i, test_case) in enumerate(test_cases):
|
|
18
|
+
(text, expected) = test_case
|
|
19
|
+
|
|
20
|
+
with self.subTest(msg = f"Case {i} ('{text}'):"):
|
|
21
|
+
actual = edq.util.hash.sha256_hex(text)
|
|
22
|
+
self.assertEqual(expected, actual)
|
|
23
|
+
|
|
24
|
+
def test_clip_text_base(self):
|
|
25
|
+
""" Test the base functionality of clip_text(). """
|
|
26
|
+
|
|
27
|
+
# [(text, max length, kwargs, expected), ...]
|
|
28
|
+
test_cases = [
|
|
29
|
+
# No Clip
|
|
30
|
+
(
|
|
31
|
+
'abcdefghijklmnopqrstuvwxyz1234567890',
|
|
32
|
+
100,
|
|
33
|
+
{},
|
|
34
|
+
'abcdefghijklmnopqrstuvwxyz1234567890',
|
|
35
|
+
),
|
|
36
|
+
|
|
37
|
+
# Base Clip
|
|
38
|
+
(
|
|
39
|
+
'abcdefghijklmnopqrstuvwxyz1234567890',
|
|
40
|
+
30,
|
|
41
|
+
{},
|
|
42
|
+
'abcdefg[text clipped 77d721c8]',
|
|
43
|
+
),
|
|
44
|
+
|
|
45
|
+
# Full Clip
|
|
46
|
+
(
|
|
47
|
+
'abcdefghijklmnopqrstuvwxyz1234567890',
|
|
48
|
+
23,
|
|
49
|
+
{},
|
|
50
|
+
'[text clipped 77d721c8]',
|
|
51
|
+
),
|
|
52
|
+
|
|
53
|
+
# Over Clip
|
|
54
|
+
(
|
|
55
|
+
'abcdefghijklmnopqrstuvwxyz1234567890',
|
|
56
|
+
10,
|
|
57
|
+
{},
|
|
58
|
+
'[text clipped 77d721c8]',
|
|
59
|
+
),
|
|
60
|
+
|
|
61
|
+
# Different Hash Length
|
|
62
|
+
(
|
|
63
|
+
'abcdefghijklmnopqrstuvwxyz1234567890',
|
|
64
|
+
30,
|
|
65
|
+
{'hash_length': 10},
|
|
66
|
+
'abcde[text clipped 77d721c817]',
|
|
67
|
+
),
|
|
68
|
+
|
|
69
|
+
# Notification Longer Than Text
|
|
70
|
+
(
|
|
71
|
+
'abc',
|
|
72
|
+
1,
|
|
73
|
+
{},
|
|
74
|
+
'abc',
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
'abcdefghijklmnopqrstuvwxyz1234567890',
|
|
78
|
+
10,
|
|
79
|
+
{'hash_length': 64},
|
|
80
|
+
'abcdefghijklmnopqrstuvwxyz1234567890',
|
|
81
|
+
),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
for (i, test_case) in enumerate(test_cases):
|
|
85
|
+
(text, max_length, kwargs, expected) = test_case
|
|
86
|
+
|
|
87
|
+
with self.subTest(msg = f"Case {i} ('{text}'):"):
|
|
88
|
+
actual = edq.util.hash.clip_text(text, max_length, **kwargs)
|
|
89
|
+
self.assertEqual(expected, actual)
|
edq/util/json.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file standardizes how we write and read JSON.
|
|
3
|
+
Specifically, we try to be flexible when reading (using JSON5),
|
|
4
|
+
and strict when writing (using vanilla JSON).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import enum
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import typing
|
|
11
|
+
|
|
12
|
+
import json5
|
|
13
|
+
|
|
14
|
+
import edq.util.dirent
|
|
15
|
+
|
|
16
|
+
class DictConverter():
|
|
17
|
+
"""
|
|
18
|
+
A base class for class that can represent (serialize) and reconstruct (deserialize) themselves as/from a dict.
|
|
19
|
+
The intention is that the dict can then be cleanly converted to/from JSON.
|
|
20
|
+
|
|
21
|
+
General (but inefficient) implementations of several core Python equality, comparison, and representation methods are provided.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> typing.Dict[str, typing.Any]:
|
|
25
|
+
"""
|
|
26
|
+
Return a dict that can be used to represent this object.
|
|
27
|
+
If the dict is passed to from_dict(), an identical object should be reconstructed.
|
|
28
|
+
|
|
29
|
+
A general (but inefficient) implementation is provided by default.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
return vars(self).copy()
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
# Note that `typing.Self` is returned, but that is introduced in Python 3.12.
|
|
36
|
+
def from_dict(cls, data: typing.Dict[str, typing.Any]) -> typing.Any:
|
|
37
|
+
"""
|
|
38
|
+
Return an instance of this subclass created using the given dict.
|
|
39
|
+
If the dict came from to_dict(), the returned object should be identical to the original.
|
|
40
|
+
|
|
41
|
+
A general (but inefficient) implementation is provided by default.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
return cls(**data)
|
|
45
|
+
|
|
46
|
+
def __eq__(self, other: object) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Check for equality.
|
|
49
|
+
|
|
50
|
+
This check uses to_dict() and compares the results.
|
|
51
|
+
This may not be complete or efficient depending on the child class.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# Note the hard type check (done so we can keep this method general).
|
|
55
|
+
if (type(self) != type(other)): # pylint: disable=unidiomatic-typecheck
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
return bool(self.to_dict() == other.to_dict()) # type: ignore[attr-defined]
|
|
59
|
+
|
|
60
|
+
def __lt__(self, other: 'DictConverter') -> bool:
|
|
61
|
+
return dumps(self) < dumps(other)
|
|
62
|
+
|
|
63
|
+
def __hash__(self) -> int:
|
|
64
|
+
return hash(dumps(self))
|
|
65
|
+
|
|
66
|
+
def __str__(self) -> str:
|
|
67
|
+
return dumps(self)
|
|
68
|
+
|
|
69
|
+
def __repr__(self) -> str:
|
|
70
|
+
return dumps(self)
|
|
71
|
+
|
|
72
|
+
def _custom_handle(value: typing.Any) -> typing.Union[typing.Dict[str, typing.Any], str]:
|
|
73
|
+
"""
|
|
74
|
+
Handle objects that are not JSON serializable by default,
|
|
75
|
+
e.g., calling vars() on an object.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
if (isinstance(value, DictConverter)):
|
|
79
|
+
return value.to_dict()
|
|
80
|
+
|
|
81
|
+
if (isinstance(value, enum.Enum)):
|
|
82
|
+
return str(value)
|
|
83
|
+
|
|
84
|
+
if (hasattr(value, '__dict__')):
|
|
85
|
+
return dict(vars(value))
|
|
86
|
+
|
|
87
|
+
raise ValueError(f"Could not JSON serialize object: '{value}'.")
|
|
88
|
+
|
|
89
|
+
def load(file_obj: typing.TextIO, strict: bool = False, **kwargs: typing.Any) -> typing.Any:
|
|
90
|
+
"""
|
|
91
|
+
Load a file object/handler as JSON.
|
|
92
|
+
If strict is set, then use standard Python JSON,
|
|
93
|
+
otherwise use JSON5.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
if (strict):
|
|
97
|
+
return json.load(file_obj, **kwargs)
|
|
98
|
+
|
|
99
|
+
return json5.load(file_obj, **kwargs)
|
|
100
|
+
|
|
101
|
+
def loads(text: str, strict: bool = False, **kwargs: typing.Any) -> typing.Any:
|
|
102
|
+
"""
|
|
103
|
+
Load a string as JSON.
|
|
104
|
+
If strict is set, then use standard Python JSON,
|
|
105
|
+
otherwise use JSON5.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
if (strict):
|
|
109
|
+
return json.loads(text, **kwargs)
|
|
110
|
+
|
|
111
|
+
return json5.loads(text, **kwargs)
|
|
112
|
+
|
|
113
|
+
def load_path(
|
|
114
|
+
path: str,
|
|
115
|
+
strict: bool = False,
|
|
116
|
+
encoding: str = edq.util.dirent.DEFAULT_ENCODING,
|
|
117
|
+
**kwargs: typing.Any) -> typing.Any:
|
|
118
|
+
"""
|
|
119
|
+
Load a file path as JSON.
|
|
120
|
+
If strict is set, then use standard Python JSON,
|
|
121
|
+
otherwise use JSON5.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
if (os.path.isdir(path)):
|
|
125
|
+
raise IsADirectoryError(f"Cannot open JSON file, expected a file but got a directory at '{path}'.")
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
with open(path, 'r', encoding = encoding) as file:
|
|
129
|
+
return load(file, strict = strict, **kwargs)
|
|
130
|
+
except Exception as ex:
|
|
131
|
+
raise ValueError(f"Failed to read JSON file '{path}'.") from ex
|
|
132
|
+
|
|
133
|
+
def loads_object(text: str, cls: typing.Type[DictConverter], **kwargs: typing.Any) -> DictConverter:
|
|
134
|
+
""" Load a JSON string into an object (which is a subclass of DictConverter). """
|
|
135
|
+
|
|
136
|
+
data = loads(text, **kwargs)
|
|
137
|
+
if (not isinstance(data, dict)):
|
|
138
|
+
raise ValueError(f"JSON to load into an object is not a dict, found '{type(data)}'.")
|
|
139
|
+
|
|
140
|
+
return cls.from_dict(data) # type: ignore[no-any-return]
|
|
141
|
+
|
|
142
|
+
def load_object_path(path: str, cls: typing.Type[DictConverter], **kwargs: typing.Any) -> DictConverter:
|
|
143
|
+
""" Load a JSON file into an object (which is a subclass of DictConverter). """
|
|
144
|
+
|
|
145
|
+
data = load_path(path, **kwargs)
|
|
146
|
+
if (not isinstance(data, dict)):
|
|
147
|
+
raise ValueError(f"JSON to load into an object is not a dict, found '{type(data)}'.")
|
|
148
|
+
|
|
149
|
+
return cls.from_dict(data) # type: ignore[no-any-return]
|
|
150
|
+
|
|
151
|
+
def dump(
|
|
152
|
+
data: typing.Any,
|
|
153
|
+
file_obj: typing.TextIO,
|
|
154
|
+
default: typing.Union[typing.Callable, None] = _custom_handle,
|
|
155
|
+
sort_keys: bool = True,
|
|
156
|
+
**kwargs: typing.Any) -> None:
|
|
157
|
+
""" Dump an object as a JSON file object. """
|
|
158
|
+
|
|
159
|
+
json.dump(data, file_obj, default = default, sort_keys = sort_keys, **kwargs)
|
|
160
|
+
|
|
161
|
+
def dumps(
|
|
162
|
+
data: typing.Any,
|
|
163
|
+
default: typing.Union[typing.Callable, None] = _custom_handle,
|
|
164
|
+
sort_keys: bool = True,
|
|
165
|
+
**kwargs: typing.Any) -> str:
|
|
166
|
+
""" Dump an object as a JSON string. """
|
|
167
|
+
|
|
168
|
+
return json.dumps(data, default = default, sort_keys = sort_keys, **kwargs)
|
|
169
|
+
|
|
170
|
+
def dump_path(
|
|
171
|
+
data: typing.Any,
|
|
172
|
+
path: str,
|
|
173
|
+
default: typing.Union[typing.Callable, None] = _custom_handle,
|
|
174
|
+
sort_keys: bool = True,
|
|
175
|
+
encoding: str = edq.util.dirent.DEFAULT_ENCODING,
|
|
176
|
+
**kwargs: typing.Any) -> None:
|
|
177
|
+
""" Dump an object as a JSON file. """
|
|
178
|
+
|
|
179
|
+
with open(path, 'w', encoding = encoding) as file:
|
|
180
|
+
dump(data, file, default = default, sort_keys = sort_keys, **kwargs)
|
edq/util/json_test.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import edq.testing.unittest
|
|
5
|
+
import edq.util.dirent
|
|
6
|
+
import edq.util.json
|
|
7
|
+
import edq.util.reflection
|
|
8
|
+
|
|
9
|
+
class TestJSON(edq.testing.unittest.BaseTest):
|
|
10
|
+
""" Test JSON utils. """
|
|
11
|
+
|
|
12
|
+
def test_loading_dumping_base(self):
|
|
13
|
+
"""
|
|
14
|
+
Test the family of JSON loading and dumping functions.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# [(string, dict, strict?, error_substring), ...]
|
|
18
|
+
test_cases = [
|
|
19
|
+
# Base
|
|
20
|
+
(
|
|
21
|
+
'{"a": 1}',
|
|
22
|
+
{"a": 1},
|
|
23
|
+
False,
|
|
24
|
+
None,
|
|
25
|
+
),
|
|
26
|
+
|
|
27
|
+
# Trivial - Strict
|
|
28
|
+
(
|
|
29
|
+
'{"a": 1}',
|
|
30
|
+
{"a": 1},
|
|
31
|
+
True,
|
|
32
|
+
None,
|
|
33
|
+
),
|
|
34
|
+
|
|
35
|
+
# JSON5
|
|
36
|
+
(
|
|
37
|
+
'{"a": 1,}',
|
|
38
|
+
{"a": 1},
|
|
39
|
+
False,
|
|
40
|
+
None,
|
|
41
|
+
),
|
|
42
|
+
|
|
43
|
+
# JSON5 - Strict
|
|
44
|
+
(
|
|
45
|
+
'{"a": 1,}',
|
|
46
|
+
{"a": 1},
|
|
47
|
+
True,
|
|
48
|
+
'JSONDecodeError',
|
|
49
|
+
),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# [(function, name), ...]
|
|
53
|
+
test_methods = [
|
|
54
|
+
(self._subtest_loads_dumps, 'subtest_loads_dumps'),
|
|
55
|
+
(self._subtest_load_dump, 'subtest_load_dump'),
|
|
56
|
+
(self._subtest_load_dump_path, 'subtest_load_dump_path'),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
for (test_method, test_method_name) in test_methods:
|
|
60
|
+
for (i, test_case) in enumerate(test_cases):
|
|
61
|
+
(text_content, dict_content, strict, error_substring) = test_case
|
|
62
|
+
|
|
63
|
+
with self.subTest(msg = f"Subtest {test_method_name}, Case {i} ('{text_content}'):"):
|
|
64
|
+
try:
|
|
65
|
+
test_method(text_content, dict_content, strict)
|
|
66
|
+
except AssertionError:
|
|
67
|
+
# The subttest failed an assertion.
|
|
68
|
+
raise
|
|
69
|
+
except Exception as ex:
|
|
70
|
+
error_string = self.format_error_string(ex)
|
|
71
|
+
if (error_substring is None):
|
|
72
|
+
self.fail(f"Unexpected error: '{error_string}'.")
|
|
73
|
+
|
|
74
|
+
self.assertIn(error_substring, error_string, 'Error is not as expected.')
|
|
75
|
+
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
if (error_substring is not None):
|
|
79
|
+
self.fail(f"Did not get expected error: '{error_substring}'.")
|
|
80
|
+
|
|
81
|
+
def _subtest_loads_dumps(self, text_content, dict_content, strict):
|
|
82
|
+
actual_dict = edq.util.json.loads(text_content, strict = strict)
|
|
83
|
+
actual_text = edq.util.json.dumps(dict_content)
|
|
84
|
+
double_conversion_text = edq.util.json.dumps(actual_dict)
|
|
85
|
+
|
|
86
|
+
self.assertDictEqual(dict_content, actual_dict)
|
|
87
|
+
self.assertEqual(actual_text, double_conversion_text)
|
|
88
|
+
|
|
89
|
+
def _subtest_load_dump(self, text_content, dict_content, strict):
|
|
90
|
+
temp_dir = edq.util.dirent.get_temp_dir(prefix = 'edq_test_json_')
|
|
91
|
+
|
|
92
|
+
path_text = os.path.join(temp_dir, 'test-text.json')
|
|
93
|
+
path_dict = os.path.join(temp_dir, 'test-dict.json')
|
|
94
|
+
|
|
95
|
+
edq.util.dirent.write_file(path_text, text_content)
|
|
96
|
+
|
|
97
|
+
with open(path_text, 'r', encoding = edq.util.dirent.DEFAULT_ENCODING) as file:
|
|
98
|
+
text_load = edq.util.json.load(file, strict = strict)
|
|
99
|
+
|
|
100
|
+
with open(path_dict, 'w', encoding = edq.util.dirent.DEFAULT_ENCODING) as file:
|
|
101
|
+
edq.util.json.dump(dict_content, file)
|
|
102
|
+
|
|
103
|
+
with open(path_dict, 'r', encoding = edq.util.dirent.DEFAULT_ENCODING) as file:
|
|
104
|
+
dict_load = edq.util.json.load(file, strict = strict)
|
|
105
|
+
|
|
106
|
+
self.assertDictEqual(dict_content, text_load)
|
|
107
|
+
self.assertDictEqual(dict_load, text_load)
|
|
108
|
+
|
|
109
|
+
def _subtest_load_dump_path(self, text_content, dict_content, strict):
|
|
110
|
+
temp_dir = edq.util.dirent.get_temp_dir(prefix = 'edq_test_json_path_')
|
|
111
|
+
|
|
112
|
+
path_text = os.path.join(temp_dir, 'test-text.json')
|
|
113
|
+
path_dict = os.path.join(temp_dir, 'test-dict.json')
|
|
114
|
+
|
|
115
|
+
edq.util.dirent.write_file(path_text, text_content)
|
|
116
|
+
text_load = edq.util.json.load_path(path_text, strict = strict)
|
|
117
|
+
|
|
118
|
+
edq.util.json.dump_path(dict_content, path_dict)
|
|
119
|
+
dict_load = edq.util.json.load_path(path_dict, strict = strict)
|
|
120
|
+
|
|
121
|
+
self.assertDictEqual(dict_content, text_load)
|
|
122
|
+
self.assertDictEqual(dict_load, text_load)
|
|
123
|
+
|
|
124
|
+
def test_object_base(self):
|
|
125
|
+
"""
|
|
126
|
+
Test loading and dumping JSON objects
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
# [(string, object, error_substring), ...]
|
|
130
|
+
test_cases = [
|
|
131
|
+
# Base
|
|
132
|
+
(
|
|
133
|
+
'{"a": 1, "b": "b"}',
|
|
134
|
+
_TestConverter(1, "b"),
|
|
135
|
+
None,
|
|
136
|
+
),
|
|
137
|
+
|
|
138
|
+
# Missing Key
|
|
139
|
+
(
|
|
140
|
+
'{"a": 1}',
|
|
141
|
+
_TestConverter(1, None),
|
|
142
|
+
None,
|
|
143
|
+
),
|
|
144
|
+
|
|
145
|
+
# Empty
|
|
146
|
+
(
|
|
147
|
+
'{}',
|
|
148
|
+
_TestConverter(None, None),
|
|
149
|
+
None,
|
|
150
|
+
),
|
|
151
|
+
|
|
152
|
+
# Extra Key
|
|
153
|
+
(
|
|
154
|
+
'{"a": 1, "b": "b", "c": 0}',
|
|
155
|
+
_TestConverter(1, "b"),
|
|
156
|
+
None,
|
|
157
|
+
),
|
|
158
|
+
|
|
159
|
+
# List
|
|
160
|
+
(
|
|
161
|
+
'[{"a": 1, "b": "b"}]',
|
|
162
|
+
_TestConverter(1, "b"),
|
|
163
|
+
'not a dict',
|
|
164
|
+
),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
# [(function, name), ...]
|
|
168
|
+
test_methods = [
|
|
169
|
+
(self._subtest_loads_object, 'subtest_loads_object'),
|
|
170
|
+
(self._subtest_load_object_path, 'subtest_load_object_path'),
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
for (test_method, test_method_name) in test_methods:
|
|
174
|
+
for (i, test_case) in enumerate(test_cases):
|
|
175
|
+
(text_content, object_content, error_substring) = test_case
|
|
176
|
+
|
|
177
|
+
with self.subTest(msg = f"Subtest {test_method_name}, Case {i} ('{text_content}'):"):
|
|
178
|
+
try:
|
|
179
|
+
test_method(text_content, object_content)
|
|
180
|
+
except AssertionError:
|
|
181
|
+
# The subttest failed an assertion.
|
|
182
|
+
raise
|
|
183
|
+
except Exception as ex:
|
|
184
|
+
error_string = self.format_error_string(ex)
|
|
185
|
+
if (error_substring is None):
|
|
186
|
+
self.fail(f"Unexpected error: '{error_string}'.")
|
|
187
|
+
|
|
188
|
+
self.assertIn(error_substring, error_string, 'Error is not as expected.')
|
|
189
|
+
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
if (error_substring is not None):
|
|
193
|
+
self.fail(f"Did not get expected error: '{error_substring}'.")
|
|
194
|
+
|
|
195
|
+
def _subtest_loads_object(self, text_content, object_content):
|
|
196
|
+
actual_object = edq.util.json.loads_object(text_content, _TestConverter)
|
|
197
|
+
actual_text = edq.util.json.dumps(object_content)
|
|
198
|
+
double_conversion_text = edq.util.json.dumps(actual_object)
|
|
199
|
+
|
|
200
|
+
self.assertEqual(object_content, actual_object)
|
|
201
|
+
self.assertEqual(actual_text, double_conversion_text)
|
|
202
|
+
|
|
203
|
+
def _subtest_load_object_path(self, text_content, object_content):
|
|
204
|
+
temp_dir = edq.util.dirent.get_temp_dir(prefix = 'edq_test_json_object_path_')
|
|
205
|
+
|
|
206
|
+
path_text = os.path.join(temp_dir, 'test-text.json')
|
|
207
|
+
path_object = os.path.join(temp_dir, 'test-object.json')
|
|
208
|
+
|
|
209
|
+
edq.util.dirent.write_file(path_text, text_content)
|
|
210
|
+
text_load = edq.util.json.load_object_path(path_text, _TestConverter)
|
|
211
|
+
|
|
212
|
+
edq.util.json.dump_path(object_content, path_object)
|
|
213
|
+
object_load = edq.util.json.load_object_path(path_object, _TestConverter)
|
|
214
|
+
|
|
215
|
+
self.assertEqual(object_content, text_load)
|
|
216
|
+
self.assertEqual(object_load, text_load)
|
|
217
|
+
|
|
218
|
+
class _TestConverter(edq.util.json.DictConverter):
|
|
219
|
+
def __init__(self, a: typing.Union[int, None] = None, b: typing.Union[str, None] = None, **kwargs) -> None:
|
|
220
|
+
self.a: typing.Union[int, None] = a
|
|
221
|
+
self.b: typing.Union[str, None] = b
|
|
222
|
+
|
|
223
|
+
def to_dict(self) -> typing.Dict[str, typing.Any]:
|
|
224
|
+
return vars(self)
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def from_dict(cls, data: typing.Dict[str, typing.Any]) -> typing.Any:
|
|
228
|
+
return _TestConverter(**data)
|