edq-utils 0.0.2__py3-none-any.whl → 0.0.3__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.
Potentially problematic release.
This version of edq-utils might be problematic. Click here for more details.
- edq/__init__.py +5 -1
- edq/py.typed +0 -0
- edq/testing/__init__.py +3 -0
- edq/testing/run.py +34 -13
- edq/testing/unittest.py +10 -3
- edq/util/__init__.py +3 -0
- edq/util/dirent.py +35 -1
- edq/util/dirent_test.py +120 -3
- edq/util/json.py +86 -3
- edq/util/json_test.py +109 -2
- {edq_utils-0.0.2.dist-info → edq_utils-0.0.3.dist-info}/METADATA +2 -1
- {edq_utils-0.0.2.dist-info → edq_utils-0.0.3.dist-info}/RECORD +15 -13
- {edq_utils-0.0.2.dist-info → edq_utils-0.0.3.dist-info}/WHEEL +0 -0
- {edq_utils-0.0.2.dist-info → edq_utils-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {edq_utils-0.0.2.dist-info → edq_utils-0.0.3.dist-info}/top_level.txt +0 -0
edq/__init__.py
CHANGED
edq/py.typed
ADDED
|
File without changes
|
edq/testing/__init__.py
ADDED
edq/testing/run.py
CHANGED
|
@@ -11,10 +11,6 @@ import sys
|
|
|
11
11
|
import typing
|
|
12
12
|
import unittest
|
|
13
13
|
|
|
14
|
-
THIS_DIR: str = os.path.join(os.path.dirname(os.path.realpath(__file__)))
|
|
15
|
-
BASE_PACKAGE_DIR: str = os.path.join(THIS_DIR, '..')
|
|
16
|
-
PROJECT_ROOT_DIR: str = os.path.join(BASE_PACKAGE_DIR, '..')
|
|
17
|
-
|
|
18
14
|
DEFAULT_TEST_FILENAME_PATTERN: str = '*_test.py'
|
|
19
15
|
|
|
20
16
|
def _collect_tests(suite: typing.Union[unittest.TestCase, unittest.suite.TestSuite]) -> typing.List[unittest.TestCase]:
|
|
@@ -41,13 +37,22 @@ def run(args: argparse.Namespace) -> int:
|
|
|
41
37
|
Will raise if tests fail to load (e.g. syntax errors) and a suggested exit code otherwise.
|
|
42
38
|
"""
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
if (args.work_dir is not None):
|
|
41
|
+
os.chdir(args.work_dir)
|
|
42
|
+
|
|
43
|
+
if (args.path_additions is not None):
|
|
44
|
+
for path in args.path_additions:
|
|
45
|
+
sys.path.append(path)
|
|
46
|
+
|
|
47
|
+
if (args.test_dirs is None):
|
|
48
|
+
args.test_dirs = ['.']
|
|
47
49
|
|
|
48
50
|
runner = unittest.TextTestRunner(verbosity = 3)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
test_cases = []
|
|
52
|
+
|
|
53
|
+
for test_dir in args.test_dirs:
|
|
54
|
+
discovered_suite = unittest.TestLoader().discover(test_dir, pattern = args.filename_pattern)
|
|
55
|
+
test_cases += _collect_tests(discovered_suite)
|
|
51
56
|
|
|
52
57
|
tests = unittest.suite.TestSuite()
|
|
53
58
|
|
|
@@ -64,27 +69,43 @@ def run(args: argparse.Namespace) -> int:
|
|
|
64
69
|
faults = len(result.errors) + len(result.failures)
|
|
65
70
|
|
|
66
71
|
if (not result.wasSuccessful()):
|
|
67
|
-
# This value will be used as an exit status, so
|
|
72
|
+
# This value will be used as an exit status, so it should not be larger than a byte.
|
|
68
73
|
# (Some higher values are used specially, so just keep it at a round number.)
|
|
69
74
|
return max(1, min(faults, 100))
|
|
70
75
|
|
|
71
76
|
return 0
|
|
72
77
|
|
|
73
78
|
def main() -> int:
|
|
79
|
+
""" Parse the CLI arguments and run tests. """
|
|
80
|
+
|
|
74
81
|
args = _get_parser().parse_args()
|
|
75
82
|
return run(args)
|
|
76
83
|
|
|
77
84
|
def _get_parser() -> argparse.ArgumentParser:
|
|
85
|
+
""" Build a parser for CLI arguments. """
|
|
86
|
+
|
|
78
87
|
parser = argparse.ArgumentParser(description = 'Run unit tests discovered in this project.')
|
|
79
88
|
|
|
80
|
-
parser.add_argument('
|
|
81
|
-
action = 'store', type = str, default =
|
|
82
|
-
help = '
|
|
89
|
+
parser.add_argument('--work-dir', dest = 'work_dir',
|
|
90
|
+
action = 'store', type = str, default = os.getcwd(),
|
|
91
|
+
help = 'Set the working directory when running tests, defaults to the current working directory (%(default)s).')
|
|
92
|
+
|
|
93
|
+
parser.add_argument('--tests-dir', dest = 'test_dirs',
|
|
94
|
+
action = 'append',
|
|
95
|
+
help = 'Discover tests from these directories. Defaults to the current directory.')
|
|
96
|
+
|
|
97
|
+
parser.add_argument('--add-path', dest = 'path_additions',
|
|
98
|
+
action = 'append',
|
|
99
|
+
help = 'If supplied, add this path the sys.path before running tests.')
|
|
83
100
|
|
|
84
101
|
parser.add_argument('--filename-pattern', dest = 'filename_pattern',
|
|
85
102
|
action = 'store', type = str, default = DEFAULT_TEST_FILENAME_PATTERN,
|
|
86
103
|
help = 'The pattern to use to find test files (default: %(default)s).')
|
|
87
104
|
|
|
105
|
+
parser.add_argument('pattern',
|
|
106
|
+
action = 'store', type = str, default = None, nargs = '?',
|
|
107
|
+
help = 'If supplied, only tests with names matching this pattern will be run. This pattern is used directly in re.search().')
|
|
108
|
+
|
|
88
109
|
return parser
|
|
89
110
|
|
|
90
111
|
if __name__ == '__main__':
|
edq/testing/unittest.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import typing
|
|
3
2
|
import unittest
|
|
4
3
|
|
|
@@ -15,13 +14,21 @@ class BaseTest(unittest.TestCase):
|
|
|
15
14
|
maxDiff = None
|
|
16
15
|
""" Don't limit the size of diffs. """
|
|
17
16
|
|
|
18
|
-
def assertJSONDictEqual(self, a: dict, b: dict) -> None:
|
|
17
|
+
def assertJSONDictEqual(self, a: dict, b: dict) -> None: # pylint: disable=invalid-name
|
|
18
|
+
"""
|
|
19
|
+
Call assertDictEqual(), but supply a message containing the full JSON representation of the arguments.
|
|
20
|
+
"""
|
|
21
|
+
|
|
19
22
|
a_json = edq.util.json.dumps(a, indent = 4)
|
|
20
23
|
b_json = edq.util.json.dumps(b, indent = 4)
|
|
21
24
|
|
|
22
25
|
super().assertDictEqual(a, b, FORMAT_STR % (a_json, b_json))
|
|
23
26
|
|
|
24
|
-
def assertJSONListEqual(self, a: list, b: list) -> None:
|
|
27
|
+
def assertJSONListEqual(self, a: list, b: list) -> None: # pylint: disable=invalid-name
|
|
28
|
+
"""
|
|
29
|
+
Call assertListEqual(), but supply a message containing the full JSON representation of the arguments.
|
|
30
|
+
"""
|
|
31
|
+
|
|
25
32
|
a_json = edq.util.json.dumps(a, indent = 4)
|
|
26
33
|
b_json = edq.util.json.dumps(b, indent = 4)
|
|
27
34
|
|
edq/util/__init__.py
CHANGED
edq/util/dirent.py
CHANGED
|
@@ -13,6 +13,7 @@ import atexit
|
|
|
13
13
|
import os
|
|
14
14
|
import shutil
|
|
15
15
|
import tempfile
|
|
16
|
+
import typing
|
|
16
17
|
import uuid
|
|
17
18
|
|
|
18
19
|
DEFAULT_ENCODING: str = 'utf-8'
|
|
@@ -247,7 +248,7 @@ def read_file(raw_path: str, strip: bool = True, encoding: str = DEFAULT_ENCODIN
|
|
|
247
248
|
return contents
|
|
248
249
|
|
|
249
250
|
def write_file(
|
|
250
|
-
raw_path: str, contents: str,
|
|
251
|
+
raw_path: str, contents: typing.Union[str, None],
|
|
251
252
|
strip: bool = True, newline: bool = True,
|
|
252
253
|
encoding: str = DEFAULT_ENCODING,
|
|
253
254
|
no_clobber = False) -> None:
|
|
@@ -276,6 +277,39 @@ def write_file(
|
|
|
276
277
|
with open(path, 'w', encoding = encoding) as file:
|
|
277
278
|
file.write(contents)
|
|
278
279
|
|
|
280
|
+
def read_file_bytes(raw_path: str) -> bytes:
|
|
281
|
+
""" Read the contents of a file as bytes. """
|
|
282
|
+
|
|
283
|
+
path = os.path.abspath(raw_path)
|
|
284
|
+
|
|
285
|
+
if (not exists(path)):
|
|
286
|
+
raise ValueError(f"Source of read bytes does not exist: '{raw_path}'.")
|
|
287
|
+
|
|
288
|
+
with open(path, 'rb') as file:
|
|
289
|
+
return file.read()
|
|
290
|
+
|
|
291
|
+
def write_file_bytes(
|
|
292
|
+
raw_path: str, contents: typing.Union[bytes, None],
|
|
293
|
+
no_clobber = False) -> None:
|
|
294
|
+
"""
|
|
295
|
+
Write the contents of a file as bytes.
|
|
296
|
+
If clobbering, any existing dirent will be removed before write.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
path = os.path.abspath(raw_path)
|
|
300
|
+
|
|
301
|
+
if (exists(path)):
|
|
302
|
+
if (no_clobber):
|
|
303
|
+
raise ValueError(f"Destination of write bytes already exists: '{raw_path}'.")
|
|
304
|
+
|
|
305
|
+
remove(path)
|
|
306
|
+
|
|
307
|
+
if (contents is None):
|
|
308
|
+
contents = b''
|
|
309
|
+
|
|
310
|
+
with open(path, 'wb') as file:
|
|
311
|
+
file.write(contents)
|
|
312
|
+
|
|
279
313
|
def contains_path(parent: str, child: str) -> bool:
|
|
280
314
|
"""
|
|
281
315
|
Check if the parent path contains the child path.
|
edq/util/dirent_test.py
CHANGED
|
@@ -92,6 +92,124 @@ class TestDirent(edq.testing.unittest.BaseTest):
|
|
|
92
92
|
actual = edq.util.dirent.contains_path(parent, child)
|
|
93
93
|
self.assertEqual(expected, actual)
|
|
94
94
|
|
|
95
|
+
def test_read_write_file_bytes_base(self):
|
|
96
|
+
""" Test reading and writing a file as bytes. """
|
|
97
|
+
|
|
98
|
+
# [(path, write kwargs, read kwargs, write contents, expected contents, error substring), ...]
|
|
99
|
+
# All conent should be strings that will be encoded.
|
|
100
|
+
test_cases = [
|
|
101
|
+
# Base
|
|
102
|
+
(
|
|
103
|
+
"test.txt",
|
|
104
|
+
{},
|
|
105
|
+
{},
|
|
106
|
+
"test",
|
|
107
|
+
"test",
|
|
108
|
+
None,
|
|
109
|
+
),
|
|
110
|
+
|
|
111
|
+
# Empty Write
|
|
112
|
+
(
|
|
113
|
+
"test.txt",
|
|
114
|
+
{},
|
|
115
|
+
{},
|
|
116
|
+
"",
|
|
117
|
+
"",
|
|
118
|
+
None,
|
|
119
|
+
),
|
|
120
|
+
|
|
121
|
+
# None Write
|
|
122
|
+
(
|
|
123
|
+
"test.txt",
|
|
124
|
+
{},
|
|
125
|
+
{},
|
|
126
|
+
None,
|
|
127
|
+
"",
|
|
128
|
+
None,
|
|
129
|
+
),
|
|
130
|
+
|
|
131
|
+
# Clobber
|
|
132
|
+
(
|
|
133
|
+
"a.txt",
|
|
134
|
+
{},
|
|
135
|
+
{},
|
|
136
|
+
"test",
|
|
137
|
+
"test",
|
|
138
|
+
None,
|
|
139
|
+
),
|
|
140
|
+
(
|
|
141
|
+
"dir_1",
|
|
142
|
+
{},
|
|
143
|
+
{},
|
|
144
|
+
"test",
|
|
145
|
+
"test",
|
|
146
|
+
None,
|
|
147
|
+
),
|
|
148
|
+
(
|
|
149
|
+
"symlink_a.txt",
|
|
150
|
+
{},
|
|
151
|
+
{},
|
|
152
|
+
"test",
|
|
153
|
+
"test",
|
|
154
|
+
None,
|
|
155
|
+
),
|
|
156
|
+
|
|
157
|
+
# No Clobber
|
|
158
|
+
(
|
|
159
|
+
"a.txt",
|
|
160
|
+
{'no_clobber': True},
|
|
161
|
+
{},
|
|
162
|
+
"test",
|
|
163
|
+
"test",
|
|
164
|
+
'already exists',
|
|
165
|
+
),
|
|
166
|
+
(
|
|
167
|
+
"dir_1",
|
|
168
|
+
{'no_clobber': True},
|
|
169
|
+
{},
|
|
170
|
+
"test",
|
|
171
|
+
"test",
|
|
172
|
+
'already exists',
|
|
173
|
+
),
|
|
174
|
+
(
|
|
175
|
+
"symlink_a.txt",
|
|
176
|
+
{'no_clobber': True},
|
|
177
|
+
{},
|
|
178
|
+
"test",
|
|
179
|
+
"test",
|
|
180
|
+
'already exists',
|
|
181
|
+
),
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
for (i, test_case) in enumerate(test_cases):
|
|
185
|
+
(path, write_options, read_options, write_contents, expected_contents, error_substring) = test_case
|
|
186
|
+
|
|
187
|
+
with self.subTest(msg = f"Case {i} ('{path}'):"):
|
|
188
|
+
temp_dir = self._prep_temp_dir()
|
|
189
|
+
path = os.path.join(temp_dir, path)
|
|
190
|
+
|
|
191
|
+
if (write_contents is not None):
|
|
192
|
+
write_contents = bytes(write_contents, edq.util.dirent.DEFAULT_ENCODING)
|
|
193
|
+
|
|
194
|
+
expected_contents = bytes(expected_contents, edq.util.dirent.DEFAULT_ENCODING)
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
edq.util.dirent.write_file_bytes(path, write_contents, **write_options)
|
|
198
|
+
actual_contents = edq.util.dirent.read_file_bytes(path, **read_options)
|
|
199
|
+
except Exception as ex:
|
|
200
|
+
error_string = self.format_error_string(ex)
|
|
201
|
+
if (error_substring is None):
|
|
202
|
+
self.fail(f"Unexpected error: '{error_string}'.")
|
|
203
|
+
|
|
204
|
+
self.assertIn(error_substring, error_string, 'Error is not as expected.')
|
|
205
|
+
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
if (error_substring is not None):
|
|
209
|
+
self.fail(f"Did not get expected error: '{error_substring}'.")
|
|
210
|
+
|
|
211
|
+
self.assertEqual(expected_contents, actual_contents)
|
|
212
|
+
|
|
95
213
|
def test_read_write_file_base(self):
|
|
96
214
|
""" Test reading and writing a file. """
|
|
97
215
|
|
|
@@ -157,7 +275,7 @@ class TestDirent(edq.testing.unittest.BaseTest):
|
|
|
157
275
|
None,
|
|
158
276
|
),
|
|
159
277
|
|
|
160
|
-
# None
|
|
278
|
+
# None Write
|
|
161
279
|
(
|
|
162
280
|
"test.txt",
|
|
163
281
|
{'newline': False},
|
|
@@ -225,7 +343,6 @@ class TestDirent(edq.testing.unittest.BaseTest):
|
|
|
225
343
|
|
|
226
344
|
with self.subTest(msg = f"Case {i} ('{path}'):"):
|
|
227
345
|
temp_dir = self._prep_temp_dir()
|
|
228
|
-
|
|
229
346
|
path = os.path.join(temp_dir, path)
|
|
230
347
|
|
|
231
348
|
try:
|
|
@@ -243,7 +360,7 @@ class TestDirent(edq.testing.unittest.BaseTest):
|
|
|
243
360
|
if (error_substring is not None):
|
|
244
361
|
self.fail(f"Did not get expected error: '{error_substring}'.")
|
|
245
362
|
|
|
246
|
-
|
|
363
|
+
self.assertEqual(expected_contents, actual_contents)
|
|
247
364
|
|
|
248
365
|
def test_copy_contents_base(self):
|
|
249
366
|
"""
|
edq/util/json.py
CHANGED
|
@@ -4,6 +4,8 @@ Specifically, we try to be flexible when reading (using JSON5),
|
|
|
4
4
|
and strict when writing (using vanilla JSON).
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import abc
|
|
8
|
+
import enum
|
|
7
9
|
import json
|
|
8
10
|
import typing
|
|
9
11
|
|
|
@@ -11,6 +13,65 @@ import json5
|
|
|
11
13
|
|
|
12
14
|
import edq.util.dirent
|
|
13
15
|
|
|
16
|
+
class DictConverter(abc.ABC):
|
|
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
|
+
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
def to_dict(self) -> typing.Dict[str, typing.Any]:
|
|
24
|
+
"""
|
|
25
|
+
Return a dict that can be used to represent this object.
|
|
26
|
+
If the dict is passed to from_dict(), an identical object should be reconstructed.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
@abc.abstractmethod
|
|
31
|
+
# Note that `typing.Self` is returned, but that is introduced in Python 3.12.
|
|
32
|
+
def from_dict(cls, data: typing.Dict[str, typing.Any]) -> typing.Any:
|
|
33
|
+
"""
|
|
34
|
+
Return an instance of this subclass created using the given dict.
|
|
35
|
+
If the dict came from to_dict(), the returned object should be identical to the original.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __eq__(self, other: object) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Check for equality.
|
|
41
|
+
|
|
42
|
+
This check uses to_dict() and compares the results.
|
|
43
|
+
This may not be complete or efficient depending on the child class.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# Note the hard type check (done so we can keep this method general).
|
|
47
|
+
if (type(self) != type(other)): # pylint: disable=unidiomatic-typecheck
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
return self.to_dict() == other.to_dict() # type: ignore[attr-defined]
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
return dumps(self)
|
|
54
|
+
|
|
55
|
+
def __repr__(self) -> str:
|
|
56
|
+
return dumps(self)
|
|
57
|
+
|
|
58
|
+
def _custom_handle(value: typing.Any) -> typing.Union[typing.Dict[str, typing.Any], str]:
|
|
59
|
+
"""
|
|
60
|
+
Handle objects that are not JSON serializable by default,
|
|
61
|
+
e.g., calling vars() on an object.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
if (isinstance(value, DictConverter)):
|
|
65
|
+
return value.to_dict()
|
|
66
|
+
|
|
67
|
+
if (isinstance(value, enum.Enum)):
|
|
68
|
+
return str(value)
|
|
69
|
+
|
|
70
|
+
if (hasattr(value, '__dict__')):
|
|
71
|
+
return vars(value)
|
|
72
|
+
|
|
73
|
+
raise ValueError(f"Could not JSON serialize object: '{value}'.")
|
|
74
|
+
|
|
14
75
|
def load(file_obj: typing.TextIO, strict: bool = False, **kwargs) -> typing.Dict[str, typing.Any]:
|
|
15
76
|
"""
|
|
16
77
|
Load a file object/handler as JSON.
|
|
@@ -52,29 +113,51 @@ def load_path(
|
|
|
52
113
|
except Exception as ex:
|
|
53
114
|
raise ValueError(f"Failed to read JSON file '{path}'.") from ex
|
|
54
115
|
|
|
116
|
+
def loads_object(text: str, cls: typing.Type[DictConverter], **kwargs) -> DictConverter:
|
|
117
|
+
""" Load a JSON string into an object (which is a subclass of DictConverter). """
|
|
118
|
+
|
|
119
|
+
data = loads(text, **kwargs)
|
|
120
|
+
if (not isinstance(data, dict)):
|
|
121
|
+
raise ValueError(f"JSON to load into an object is not a dict, found '{type(data)}'.")
|
|
122
|
+
|
|
123
|
+
return cls.from_dict(data)
|
|
124
|
+
|
|
125
|
+
def load_object_path(path: str, cls: typing.Type[DictConverter], **kwargs) -> DictConverter:
|
|
126
|
+
""" Load a JSON file into an object (which is a subclass of DictConverter). """
|
|
127
|
+
|
|
128
|
+
data = load_path(path, **kwargs)
|
|
129
|
+
if (not isinstance(data, dict)):
|
|
130
|
+
raise ValueError(f"JSON to load into an object is not a dict, found '{type(data)}'.")
|
|
131
|
+
|
|
132
|
+
return cls.from_dict(data)
|
|
133
|
+
|
|
55
134
|
def dump(
|
|
56
135
|
data: typing.Any,
|
|
57
136
|
file_obj: typing.TextIO,
|
|
137
|
+
default: typing.Union[typing.Callable, None] = _custom_handle,
|
|
58
138
|
sort_keys: bool = True,
|
|
59
139
|
**kwargs) -> None:
|
|
60
140
|
""" Dump an object as a JSON file object. """
|
|
61
141
|
|
|
62
|
-
json.dump(data, file_obj, sort_keys = sort_keys, **kwargs)
|
|
142
|
+
json.dump(data, file_obj, default = default, sort_keys = sort_keys, **kwargs)
|
|
63
143
|
|
|
64
144
|
def dumps(
|
|
65
145
|
data: typing.Any,
|
|
146
|
+
default: typing.Union[typing.Callable, None] = _custom_handle,
|
|
66
147
|
sort_keys: bool = True,
|
|
67
148
|
**kwargs) -> str:
|
|
68
149
|
""" Dump an object as a JSON string. """
|
|
69
150
|
|
|
70
|
-
return json.dumps(data, sort_keys = sort_keys, **kwargs)
|
|
151
|
+
return json.dumps(data, default = default, sort_keys = sort_keys, **kwargs)
|
|
71
152
|
|
|
72
153
|
def dump_path(
|
|
73
154
|
data: typing.Any,
|
|
74
155
|
path: str,
|
|
156
|
+
default: typing.Union[typing.Callable, None] = _custom_handle,
|
|
157
|
+
sort_keys: bool = True,
|
|
75
158
|
encoding: str = edq.util.dirent.DEFAULT_ENCODING,
|
|
76
159
|
**kwargs) -> None:
|
|
77
160
|
""" Dump an object as a JSON file. """
|
|
78
161
|
|
|
79
162
|
with open(path, 'w', encoding = encoding) as file:
|
|
80
|
-
dump(data, file, **kwargs)
|
|
163
|
+
dump(data, file, default = default, sort_keys = sort_keys, **kwargs)
|
edq/util/json_test.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import typing
|
|
2
3
|
|
|
3
4
|
import edq.testing.unittest
|
|
4
5
|
import edq.util.dirent
|
|
@@ -80,10 +81,10 @@ class TestJSON(edq.testing.unittest.BaseTest):
|
|
|
80
81
|
def _subtest_loads_dumps(self, text_content, dict_content, strict):
|
|
81
82
|
actual_dict = edq.util.json.loads(text_content, strict = strict)
|
|
82
83
|
actual_text = edq.util.json.dumps(dict_content)
|
|
83
|
-
|
|
84
|
+
double_conversion_text = edq.util.json.dumps(actual_dict)
|
|
84
85
|
|
|
85
86
|
self.assertDictEqual(dict_content, actual_dict)
|
|
86
|
-
self.assertEqual(actual_text,
|
|
87
|
+
self.assertEqual(actual_text, double_conversion_text)
|
|
87
88
|
|
|
88
89
|
def _subtest_load_dump(self, text_content, dict_content, strict):
|
|
89
90
|
temp_dir = edq.util.dirent.get_temp_dir(prefix = 'edq_test_json_')
|
|
@@ -119,3 +120,109 @@ class TestJSON(edq.testing.unittest.BaseTest):
|
|
|
119
120
|
|
|
120
121
|
self.assertDictEqual(dict_content, text_load)
|
|
121
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)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edq-utils
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Common utilities used by EduLinq Python projects.
|
|
5
5
|
Author-email: Eriq Augustine <eriq@edulinq.org>
|
|
6
6
|
License: MIT License
|
|
@@ -37,6 +37,7 @@ License-File: LICENSE
|
|
|
37
37
|
Requires-Dist: json5>=0.9.14
|
|
38
38
|
Provides-Extra: dev
|
|
39
39
|
Requires-Dist: mypy>=1.14.1; extra == "dev"
|
|
40
|
+
Requires-Dist: pdoc>=14.7.0; extra == "dev"
|
|
40
41
|
Requires-Dist: pylint; extra == "dev"
|
|
41
42
|
Requires-Dist: twine; extra == "dev"
|
|
42
43
|
Requires-Dist: vermin; extra == "dev"
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
edq/__init__.py,sha256=
|
|
2
|
-
edq/
|
|
3
|
-
edq/testing/
|
|
4
|
-
edq/
|
|
5
|
-
edq/
|
|
6
|
-
edq/util/
|
|
7
|
-
edq/util/
|
|
8
|
-
edq/util/
|
|
1
|
+
edq/__init__.py,sha256=SgI3o5Z-z1KSX-sg-2ZL2vhNQ-KiMte0ewCvjQv8wWA,86
|
|
2
|
+
edq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
edq/testing/__init__.py,sha256=IKd3fPU_8d_jP19HxG-zKwxFwn7nqFGGtXOY5slY41c,32
|
|
4
|
+
edq/testing/run.py,sha256=qnqGCEwZP-hY87X99EJw2xoMaTF8QeOf7Lx8JI7oM_4,3843
|
|
5
|
+
edq/testing/unittest.py,sha256=J_sUYGIdP8HDQioQtUPF_H4n2aKf0_XHLdhzs59fvck,1665
|
|
6
|
+
edq/util/__init__.py,sha256=9EFKQE77S-B6OJJKFaMg8k3WkMMUQYlGjlTv6tQmWVo,29
|
|
7
|
+
edq/util/dirent.py,sha256=C-ZTVbOVBlEc71g4l8rMO5EJHv4Lcl9-FH0Kp1B3oc8,10314
|
|
8
|
+
edq/util/dirent_test.py,sha256=oXpAaEhOkEr0zw0fdAPypx7QH1aFbY2Hpox-9OpZjhs,33380
|
|
9
|
+
edq/util/json.py,sha256=p74F5OCxbRv4mvMSkRdy-fYVqAETx5_65IEhesTH8SM,5228
|
|
10
|
+
edq/util/json_test.py,sha256=utUVRbw3z42ke4fpRVI294RrFHcMKms8khVYRkISNk4,8009
|
|
9
11
|
edq/util/pyimport.py,sha256=M1j58vg4b6gTg92Cz5-bns3eQCCIMKDApBclP-iR620,2198
|
|
10
12
|
edq/util/pyimport_test.py,sha256=wuTR5pzVZanWDA2FuVc-Pxyo_GwkGGfFf_qyK6LNQRs,2851
|
|
11
13
|
edq/util/reflection.py,sha256=jPcW6h0fwSDYh04O5rUxlgoF7HK6fVQ2mq7DD9qPrEg,972
|
|
@@ -19,8 +21,8 @@ edq/util/testdata/dirent-operations/dir_1/b.txt,sha256=AmOCmYm2_ZVPcrqvL8ZLwuLwH
|
|
|
19
21
|
edq/util/testdata/dirent-operations/dir_1/dir_2/c.txt,sha256=o6XnFfDMV0pzw_m-u2vCTzL_1bZ7OHJEwskJ2neaFHg,2
|
|
20
22
|
edq/util/testdata/dirent-operations/symlink_dir_1/b.txt,sha256=AmOCmYm2_ZVPcrqvL8ZLwuLwHWktTecphuqAj26ZgT8,2
|
|
21
23
|
edq/util/testdata/dirent-operations/symlink_dir_1/dir_2/c.txt,sha256=o6XnFfDMV0pzw_m-u2vCTzL_1bZ7OHJEwskJ2neaFHg,2
|
|
22
|
-
edq_utils-0.0.
|
|
23
|
-
edq_utils-0.0.
|
|
24
|
-
edq_utils-0.0.
|
|
25
|
-
edq_utils-0.0.
|
|
26
|
-
edq_utils-0.0.
|
|
24
|
+
edq_utils-0.0.3.dist-info/licenses/LICENSE,sha256=MS4iYEl4rOxMoprZuc86iYVoyk4YgaVoMt7WmGvVF8w,1064
|
|
25
|
+
edq_utils-0.0.3.dist-info/METADATA,sha256=YoOLw86gdPLaSAy_z9UfftRk83wxjjM-nD7r8H2FsEg,2471
|
|
26
|
+
edq_utils-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
27
|
+
edq_utils-0.0.3.dist-info/top_level.txt,sha256=znBHSj6tgXtcMKrUVtovLli5fIEJCb7d-BMxTLRK4zk,4
|
|
28
|
+
edq_utils-0.0.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|