json-sorted 1.0.0__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.
- json_sorted-1.0.0/LICENSE.txt +21 -0
- json_sorted-1.0.0/MANIFEST.in +3 -0
- json_sorted-1.0.0/PKG-INFO +29 -0
- json_sorted-1.0.0/README.rst +7 -0
- json_sorted-1.0.0/docs/v1.0.rst +145 -0
- json_sorted-1.0.0/pyproject.toml +44 -0
- json_sorted-1.0.0/run_tests.py +17 -0
- json_sorted-1.0.0/setup.cfg +4 -0
- json_sorted-1.0.0/src/json_sorted/__init__.py +9 -0
- json_sorted-1.0.0/src/json_sorted/__main__.py +4 -0
- json_sorted-1.0.0/src/json_sorted/core/__init__.py +0 -0
- json_sorted-1.0.0/src/json_sorted/core/main.py +67 -0
- json_sorted-1.0.0/src/json_sorted/core/run.py +173 -0
- json_sorted-1.0.0/src/json_sorted/enum/Instruction.py +8 -0
- json_sorted-1.0.0/src/json_sorted/enum/Selector.py +8 -0
- json_sorted-1.0.0/src/json_sorted/py.typed +0 -0
- json_sorted-1.0.0/src/json_sorted.egg-info/PKG-INFO +29 -0
- json_sorted-1.0.0/src/json_sorted.egg-info/SOURCES.txt +24 -0
- json_sorted-1.0.0/src/json_sorted.egg-info/dependency_links.txt +1 -0
- json_sorted-1.0.0/src/json_sorted.egg-info/requires.txt +1 -0
- json_sorted-1.0.0/src/json_sorted.egg-info/top_level.txt +1 -0
- json_sorted-1.0.0/tests/test_0.py +58 -0
- json_sorted-1.0.0/tests/test_1.py +152 -0
- json_sorted-1.0.0/tests/test_2.py +127 -0
- json_sorted-1.0.0/tests/test_3.py +44 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johannes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: json_sorted
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: This project sorts JSON data.
|
|
5
|
+
Author-email: Johannes <johannes.programming@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Download, https://pypi.org/project/json_sorted/#files
|
|
8
|
+
Project-URL: Index, https://pypi.org/project/json_sorted/
|
|
9
|
+
Project-URL: Source, https://github.com/johannes-programming/json_sorted/
|
|
10
|
+
Project-URL: Website, https://json-sorted.johannes-programming.online/
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/x-rst
|
|
19
|
+
License-File: LICENSE.txt
|
|
20
|
+
Requires-Dist: setdoc<3,>=1.3.14
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
===========
|
|
24
|
+
json_sorted
|
|
25
|
+
===========
|
|
26
|
+
|
|
27
|
+
Each minor version has its own documentation.
|
|
28
|
+
These docs can be found as rst-files in the ``docs/`` directory of this project.
|
|
29
|
+
They can also be viewed on the website `https://json-sorted.johannes-programming.online/ <https://json-sorted.johannes-programming.online/>`_.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
===========
|
|
2
|
+
json_sorted
|
|
3
|
+
===========
|
|
4
|
+
|
|
5
|
+
Each minor version has its own documentation.
|
|
6
|
+
These docs can be found as rst-files in the ``docs/`` directory of this project.
|
|
7
|
+
They can also be viewed on the website `https://json-sorted.johannes-programming.online/ <https://json-sorted.johannes-programming.online/>`_.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Overview
|
|
2
|
+
--------
|
|
3
|
+
|
|
4
|
+
This project sorts JSON data.
|
|
5
|
+
|
|
6
|
+
Installation
|
|
7
|
+
------------
|
|
8
|
+
|
|
9
|
+
To install ``json_sorted``, you can use ``pip``.
|
|
10
|
+
Open your terminal and run:
|
|
11
|
+
|
|
12
|
+
.. code-block:: shell
|
|
13
|
+
|
|
14
|
+
pip install json_sorted
|
|
15
|
+
|
|
16
|
+
Typing
|
|
17
|
+
------
|
|
18
|
+
|
|
19
|
+
This project is strictly typed with ``mypy``.
|
|
20
|
+
|
|
21
|
+
Features
|
|
22
|
+
--------
|
|
23
|
+
|
|
24
|
+
``json_sorted.core``
|
|
25
|
+
''''''''''''''''''''
|
|
26
|
+
|
|
27
|
+
``json_sorted.core.main.main(args: Optional[Iterable[str]] = None, /) -> None``
|
|
28
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
29
|
+
|
|
30
|
+
This function is the CLI implementation of ``run``.
|
|
31
|
+
|
|
32
|
+
It accepts these instruction flags, applied in the order given:
|
|
33
|
+
|
|
34
|
+
.. code-block:: shell
|
|
35
|
+
|
|
36
|
+
python -m json_sorted example.json --sort --key=foo --all-keys --index=0
|
|
37
|
+
|
|
38
|
+
turns
|
|
39
|
+
|
|
40
|
+
.. code-block:: json
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
"foo": {
|
|
44
|
+
"bar": [
|
|
45
|
+
[
|
|
46
|
+
4,
|
|
47
|
+
2
|
|
48
|
+
],
|
|
49
|
+
{}
|
|
50
|
+
],
|
|
51
|
+
"baz": [
|
|
52
|
+
{
|
|
53
|
+
"a": 9,
|
|
54
|
+
"c": 8,
|
|
55
|
+
"b": 7
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
into
|
|
62
|
+
|
|
63
|
+
.. code-block:: json
|
|
64
|
+
|
|
65
|
+
# example.json
|
|
66
|
+
{
|
|
67
|
+
"foo": {
|
|
68
|
+
"bar": [
|
|
69
|
+
[
|
|
70
|
+
2,
|
|
71
|
+
4
|
|
72
|
+
],
|
|
73
|
+
{}
|
|
74
|
+
],
|
|
75
|
+
"baz": [
|
|
76
|
+
{
|
|
77
|
+
"a": 9,
|
|
78
|
+
"b": 7,
|
|
79
|
+
"c": 8
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
``json_sorted.core.run.run(*filepatterns: str, instructions: Iterable[Instruction | Selector | int | str] = ()) -> None``
|
|
85
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
86
|
+
|
|
87
|
+
``json_sorted.enum``
|
|
88
|
+
''''''''''''''''''''
|
|
89
|
+
|
|
90
|
+
``class json_sorted.enum.Instruction.Instruction``
|
|
91
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
92
|
+
|
|
93
|
+
``SORT``
|
|
94
|
+
^^^^^^^^
|
|
95
|
+
|
|
96
|
+
``SORT_REVERSE``
|
|
97
|
+
^^^^^^^^^^^^^^^^
|
|
98
|
+
|
|
99
|
+
``class json_sorted.enum.Selector.Selector``
|
|
100
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
101
|
+
|
|
102
|
+
``ALL_KEYS``
|
|
103
|
+
^^^^^^^^^^^^
|
|
104
|
+
|
|
105
|
+
A path token that, instead of descending into a single key,
|
|
106
|
+
expands the current table and applies the remaining path and
|
|
107
|
+
the sort to every value.
|
|
108
|
+
It is produced by the ``--all-keys`` flag.
|
|
109
|
+
|
|
110
|
+
``ALL_INDICES``
|
|
111
|
+
^^^^^^^^^^^^^^^
|
|
112
|
+
|
|
113
|
+
A path token that, instead of descending into a single index,
|
|
114
|
+
expands the current array and applies the remaining path and
|
|
115
|
+
the sort to every value.
|
|
116
|
+
It is produced by the ``--all-indices`` flag.
|
|
117
|
+
|
|
118
|
+
Testing
|
|
119
|
+
-------
|
|
120
|
+
|
|
121
|
+
This project can be tested through its ``run_tests.py`` script
|
|
122
|
+
which runs the tests in its ``tests/`` dir.
|
|
123
|
+
|
|
124
|
+
License
|
|
125
|
+
-------
|
|
126
|
+
|
|
127
|
+
This project is licensed under the MIT License.
|
|
128
|
+
|
|
129
|
+
Links
|
|
130
|
+
-----
|
|
131
|
+
|
|
132
|
+
- Download: https://pypi.org/project/json_sorted/#files
|
|
133
|
+
- Index: https://pypi.org/project/json_sorted/
|
|
134
|
+
- Source: https://github.com/johannes-programming/json_sorted/
|
|
135
|
+
- Website: https://json-sorted.johannes-programming.online/
|
|
136
|
+
|
|
137
|
+
Impressum
|
|
138
|
+
---------
|
|
139
|
+
|
|
140
|
+
**Johannes Programming**
|
|
141
|
+
|
|
142
|
+
- Name: Johannes
|
|
143
|
+
- Email: johannes.programming@gmail.com
|
|
144
|
+
- Homepage: https://www.johannes-programming.online/
|
|
145
|
+
- Gravatar: https://www.johannes-programming.fyi/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "setuptools.build_meta"
|
|
3
|
+
requires = [
|
|
4
|
+
"setuptools>=77.0",
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
authors = [
|
|
9
|
+
{ email = "johannes.programming@gmail.com", name = "Johannes" },
|
|
10
|
+
]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
13
|
+
"Natural Language :: English",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"setdoc>=1.3.14,<3",
|
|
21
|
+
]
|
|
22
|
+
description = "This project sorts JSON data."
|
|
23
|
+
keywords = []
|
|
24
|
+
license = "MIT"
|
|
25
|
+
license-files = [
|
|
26
|
+
"LICENSE.txt",
|
|
27
|
+
]
|
|
28
|
+
name = "json_sorted"
|
|
29
|
+
readme = "README.rst"
|
|
30
|
+
requires-python = ">=3.11"
|
|
31
|
+
version = "1.0.0"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Download = "https://pypi.org/project/json_sorted/#files"
|
|
35
|
+
Index = "https://pypi.org/project/json_sorted/"
|
|
36
|
+
Source = "https://github.com/johannes-programming/json_sorted/"
|
|
37
|
+
Website = "https://json-sorted.johannes-programming.online/"
|
|
38
|
+
|
|
39
|
+
[tool.mypy]
|
|
40
|
+
files = [
|
|
41
|
+
".",
|
|
42
|
+
]
|
|
43
|
+
python_version = "3.11"
|
|
44
|
+
strict = true
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
__all__ = ["main"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> unittest.TextTestResult:
|
|
7
|
+
loader: unittest.TestLoader
|
|
8
|
+
suite: unittest.TestSuite
|
|
9
|
+
runner: unittest.TextTestRunner
|
|
10
|
+
loader = unittest.TestLoader()
|
|
11
|
+
suite = loader.discover("tests")
|
|
12
|
+
runner = unittest.TextTestRunner(verbosity=2)
|
|
13
|
+
return runner.run(suite)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
main()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from json_sorted.core.main import main
|
|
2
|
+
from json_sorted.core.run import run
|
|
3
|
+
from json_sorted.enum.Instruction import Instruction
|
|
4
|
+
from json_sorted.enum.Selector import Selector
|
|
5
|
+
|
|
6
|
+
__all__ = ["Instruction", "Selector", "main", "run"]
|
|
7
|
+
|
|
8
|
+
if __name__ == "__main__":
|
|
9
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import setdoc
|
|
8
|
+
|
|
9
|
+
from ..enum.Instruction import Instruction
|
|
10
|
+
from ..enum.Selector import Selector
|
|
11
|
+
from .run import run
|
|
12
|
+
|
|
13
|
+
__all__ = ["main"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@setdoc.basic
|
|
17
|
+
def main(args: Optional[Iterable[str]] = None, /) -> None:
|
|
18
|
+
kwargs: dict[str, Any]
|
|
19
|
+
parser: argparse.ArgumentParser
|
|
20
|
+
parser = argparse.ArgumentParser(fromfile_prefix_chars="@")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"filepatterns",
|
|
23
|
+
nargs="*",
|
|
24
|
+
default=[],
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--all-indices",
|
|
28
|
+
action="append_const",
|
|
29
|
+
const=Selector.ALL_INDICES,
|
|
30
|
+
dest="instructions",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--all-keys",
|
|
34
|
+
action="append_const",
|
|
35
|
+
const=Selector.ALL_KEYS,
|
|
36
|
+
dest="instructions",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--key",
|
|
40
|
+
action="append",
|
|
41
|
+
dest="instructions",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--index",
|
|
45
|
+
action="append",
|
|
46
|
+
dest="instructions",
|
|
47
|
+
type=int,
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--sort",
|
|
51
|
+
action="append_const",
|
|
52
|
+
const=Instruction.SORT,
|
|
53
|
+
dest="instructions",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--sort-reverse",
|
|
57
|
+
action="append_const",
|
|
58
|
+
const=Instruction.SORT_REVERSE,
|
|
59
|
+
dest="instructions",
|
|
60
|
+
)
|
|
61
|
+
parser.set_defaults(instructions=[])
|
|
62
|
+
kwargs = vars(parser.parse_args(args))
|
|
63
|
+
try:
|
|
64
|
+
run(*kwargs.pop("filepatterns"), **kwargs)
|
|
65
|
+
except Exception:
|
|
66
|
+
logging.exception("json_sort failed!")
|
|
67
|
+
sys.exit(1)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
import setdoc
|
|
8
|
+
|
|
9
|
+
from json_sorted.enum.Instruction import Instruction
|
|
10
|
+
from json_sorted.enum.Selector import Selector
|
|
11
|
+
|
|
12
|
+
__all__ = ["run"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_absfiles(filepatterns: Iterable[str]) -> list[str]:
|
|
16
|
+
absfile: str
|
|
17
|
+
absfiles: list[str]
|
|
18
|
+
file: str
|
|
19
|
+
pattern: str
|
|
20
|
+
absfiles = list()
|
|
21
|
+
for pattern in filepatterns:
|
|
22
|
+
for file in glob.iglob(pattern, recursive=True):
|
|
23
|
+
absfile = os.path.abspath(file)
|
|
24
|
+
if absfile in absfiles:
|
|
25
|
+
continue
|
|
26
|
+
if os.path.isfile(absfile):
|
|
27
|
+
absfiles.append(absfile)
|
|
28
|
+
return absfiles
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_instructions(
|
|
32
|
+
instructions: Iterable[Instruction | Selector | int | str] = (),
|
|
33
|
+
) -> list[tuple[Instruction, list[Selector | int | str]]]:
|
|
34
|
+
ans: list[tuple[Instruction, list[Selector | int | str]]]
|
|
35
|
+
x: Instruction | Selector | int | str
|
|
36
|
+
ans = list()
|
|
37
|
+
for x in instructions:
|
|
38
|
+
if isinstance(x, Instruction):
|
|
39
|
+
ans.append((x, list()))
|
|
40
|
+
elif len(ans):
|
|
41
|
+
ans[-1][1].append(x)
|
|
42
|
+
return ans
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@setdoc.basic
|
|
46
|
+
def run(
|
|
47
|
+
*filepatterns: str,
|
|
48
|
+
instructions: Iterable[Instruction | Selector | int | str] = (),
|
|
49
|
+
) -> None:
|
|
50
|
+
absfiles: list[str]
|
|
51
|
+
parsed: list[tuple[Instruction, list[Selector | int | str]]]
|
|
52
|
+
parsed = parse_instructions(instructions)
|
|
53
|
+
absfiles = get_absfiles(filepatterns)
|
|
54
|
+
run_instructions_on_files(absfiles=absfiles, parsed=parsed)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_instruction_on_data(
|
|
58
|
+
*,
|
|
59
|
+
instruction: Instruction,
|
|
60
|
+
keys: list[Selector | int | str],
|
|
61
|
+
data: dict[str, Any],
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
return cast(
|
|
64
|
+
dict[str, Any],
|
|
65
|
+
run_instruction_along_keys(
|
|
66
|
+
data,
|
|
67
|
+
instruction=instruction,
|
|
68
|
+
keys=list(keys),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_instruction_along_keys(
|
|
74
|
+
data: Any,
|
|
75
|
+
*,
|
|
76
|
+
instruction: Instruction,
|
|
77
|
+
keys: list[Selector | int | str],
|
|
78
|
+
) -> Any:
|
|
79
|
+
head: Selector | int | str
|
|
80
|
+
rest: list[Selector | int | str]
|
|
81
|
+
if not len(keys):
|
|
82
|
+
return run_instruction_on_value(data, reverse=instruction.value)
|
|
83
|
+
head = keys[0]
|
|
84
|
+
rest = keys[1:]
|
|
85
|
+
if head is Selector.ALL_KEYS:
|
|
86
|
+
return run_instruction_on_all_keys(
|
|
87
|
+
data,
|
|
88
|
+
instruction=instruction,
|
|
89
|
+
keys=rest,
|
|
90
|
+
)
|
|
91
|
+
if head is Selector.ALL_INDICES:
|
|
92
|
+
return run_instruction_on_all_indices(
|
|
93
|
+
data,
|
|
94
|
+
instruction=instruction,
|
|
95
|
+
keys=rest,
|
|
96
|
+
)
|
|
97
|
+
data[head] = run_instruction_along_keys(
|
|
98
|
+
data[head],
|
|
99
|
+
instruction=instruction,
|
|
100
|
+
keys=rest,
|
|
101
|
+
)
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def run_instruction_on_all_keys(
|
|
106
|
+
data: Any,
|
|
107
|
+
*,
|
|
108
|
+
instruction: Instruction,
|
|
109
|
+
keys: list[Selector | int | str],
|
|
110
|
+
) -> Any:
|
|
111
|
+
key: Any
|
|
112
|
+
if isinstance(data, dict):
|
|
113
|
+
for key in list(data.keys()):
|
|
114
|
+
data[key] = run_instruction_along_keys(
|
|
115
|
+
data[key],
|
|
116
|
+
instruction=instruction,
|
|
117
|
+
keys=keys,
|
|
118
|
+
)
|
|
119
|
+
return data
|
|
120
|
+
raise TypeError(
|
|
121
|
+
f"Value {data!r} of type {type(data).__name__} has no keys to expand!"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run_instruction_on_all_indices(
|
|
126
|
+
data: Any,
|
|
127
|
+
*,
|
|
128
|
+
instruction: Instruction,
|
|
129
|
+
keys: list[Selector | int | str],
|
|
130
|
+
) -> Any:
|
|
131
|
+
index: int
|
|
132
|
+
if isinstance(data, list):
|
|
133
|
+
for index in range(len(data)):
|
|
134
|
+
data[index] = run_instruction_along_keys(
|
|
135
|
+
data[index],
|
|
136
|
+
instruction=instruction,
|
|
137
|
+
keys=keys,
|
|
138
|
+
)
|
|
139
|
+
return data
|
|
140
|
+
raise TypeError(
|
|
141
|
+
f"Value {data!r} of type {type(data).__name__} has no indices to expand!"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def run_instruction_on_value(data: Any, *, reverse: bool) -> Any:
|
|
146
|
+
if isinstance(data, dict):
|
|
147
|
+
return dict(sorted(data.items(), reverse=reverse))
|
|
148
|
+
if isinstance(data, list):
|
|
149
|
+
return list(sorted(data, reverse=reverse))
|
|
150
|
+
raise TypeError(
|
|
151
|
+
f"Value {data!r} of type {type(data).__name__} cannot be sorted!"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def run_instructions_on_files(
|
|
156
|
+
*,
|
|
157
|
+
absfiles: list[str],
|
|
158
|
+
parsed: list[tuple[Instruction, list[Selector | int | str]]],
|
|
159
|
+
) -> None:
|
|
160
|
+
absfile: str
|
|
161
|
+
data: Any
|
|
162
|
+
for absfile in absfiles:
|
|
163
|
+
with open(absfile, "r", encoding="utf-8") as stream:
|
|
164
|
+
data = json.load(stream)
|
|
165
|
+
for instruction, keys in parsed:
|
|
166
|
+
data = run_instruction_on_data(
|
|
167
|
+
data=data,
|
|
168
|
+
instruction=instruction,
|
|
169
|
+
keys=keys,
|
|
170
|
+
)
|
|
171
|
+
with open(absfile, "w", encoding="utf-8") as stream:
|
|
172
|
+
json.dump(data, stream, indent=4)
|
|
173
|
+
stream.write("\n")
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: json_sorted
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: This project sorts JSON data.
|
|
5
|
+
Author-email: Johannes <johannes.programming@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Download, https://pypi.org/project/json_sorted/#files
|
|
8
|
+
Project-URL: Index, https://pypi.org/project/json_sorted/
|
|
9
|
+
Project-URL: Source, https://github.com/johannes-programming/json_sorted/
|
|
10
|
+
Project-URL: Website, https://json-sorted.johannes-programming.online/
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/x-rst
|
|
19
|
+
License-File: LICENSE.txt
|
|
20
|
+
Requires-Dist: setdoc<3,>=1.3.14
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
===========
|
|
24
|
+
json_sorted
|
|
25
|
+
===========
|
|
26
|
+
|
|
27
|
+
Each minor version has its own documentation.
|
|
28
|
+
These docs can be found as rst-files in the ``docs/`` directory of this project.
|
|
29
|
+
They can also be viewed on the website `https://json-sorted.johannes-programming.online/ <https://json-sorted.johannes-programming.online/>`_.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.rst
|
|
4
|
+
pyproject.toml
|
|
5
|
+
run_tests.py
|
|
6
|
+
setup.cfg
|
|
7
|
+
docs/v1.0.rst
|
|
8
|
+
src/json_sorted/__init__.py
|
|
9
|
+
src/json_sorted/__main__.py
|
|
10
|
+
src/json_sorted/py.typed
|
|
11
|
+
src/json_sorted.egg-info/PKG-INFO
|
|
12
|
+
src/json_sorted.egg-info/SOURCES.txt
|
|
13
|
+
src/json_sorted.egg-info/dependency_links.txt
|
|
14
|
+
src/json_sorted.egg-info/requires.txt
|
|
15
|
+
src/json_sorted.egg-info/top_level.txt
|
|
16
|
+
src/json_sorted/core/__init__.py
|
|
17
|
+
src/json_sorted/core/main.py
|
|
18
|
+
src/json_sorted/core/run.py
|
|
19
|
+
src/json_sorted/enum/Instruction.py
|
|
20
|
+
src/json_sorted/enum/Selector.py
|
|
21
|
+
tests/test_0.py
|
|
22
|
+
tests/test_1.py
|
|
23
|
+
tests/test_2.py
|
|
24
|
+
tests/test_3.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
setdoc<3,>=1.3.14
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
json_sorted
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Self
|
|
6
|
+
|
|
7
|
+
from json_sorted.core.run import run
|
|
8
|
+
from json_sorted.enum.Instruction import Instruction
|
|
9
|
+
|
|
10
|
+
__all__ = ["TestJsonSort"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestJsonSort(unittest.TestCase):
|
|
14
|
+
|
|
15
|
+
def test_run_sorts_matching_json_file(self: Self) -> None:
|
|
16
|
+
path: Path
|
|
17
|
+
tmpdir: str
|
|
18
|
+
stream: Any
|
|
19
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
20
|
+
path = Path(tmpdir) / "example.json"
|
|
21
|
+
|
|
22
|
+
with path.open("w") as stream:
|
|
23
|
+
json.dump({"b": 2, "a": 1}, stream)
|
|
24
|
+
|
|
25
|
+
run(
|
|
26
|
+
str(path),
|
|
27
|
+
instructions=[Instruction.SORT],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
with path.open("r") as stream:
|
|
31
|
+
result = json.load(stream)
|
|
32
|
+
|
|
33
|
+
self.assertEqual(result, {"a": 1, "b": 2})
|
|
34
|
+
|
|
35
|
+
def test_run_ignores_duplicate_glob_matches(self: Self) -> None:
|
|
36
|
+
path: Path
|
|
37
|
+
stream: Any
|
|
38
|
+
tmpdir: str
|
|
39
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
40
|
+
path = Path(tmpdir) / "example.json"
|
|
41
|
+
|
|
42
|
+
with path.open("w") as stream:
|
|
43
|
+
json.dump({"b": 2, "a": 1}, stream)
|
|
44
|
+
|
|
45
|
+
run(
|
|
46
|
+
str(path),
|
|
47
|
+
str(path),
|
|
48
|
+
instructions=[Instruction.SORT],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
with path.open("r") as stream:
|
|
52
|
+
result = json.load(stream)
|
|
53
|
+
|
|
54
|
+
self.assertEqual(result, {"a": 1, "b": 2})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
unittest.main()
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
import unittest
|
|
4
|
+
from io import BufferedReader, BufferedWriter
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Self
|
|
7
|
+
|
|
8
|
+
from json_sorted.core.run import run
|
|
9
|
+
from json_sorted.enum.Instruction import Instruction
|
|
10
|
+
|
|
11
|
+
__all__ = ["TestRun"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_instruction(reverse: bool) -> Instruction:
|
|
15
|
+
instruction: Instruction
|
|
16
|
+
for instruction in Instruction:
|
|
17
|
+
if instruction.value == reverse:
|
|
18
|
+
return instruction
|
|
19
|
+
raise AssertionError(f"No Instruction found with value={reverse!r}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def write_json(path: Path, data: dict[str, Any]) -> None:
|
|
23
|
+
stream: Any
|
|
24
|
+
with path.open("w") as stream:
|
|
25
|
+
json.dump(data, stream)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def read_json(path: Path) -> Any:
|
|
29
|
+
stream: Any
|
|
30
|
+
with path.open("r") as stream:
|
|
31
|
+
return json.load(stream)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestRun(unittest.TestCase):
|
|
35
|
+
def test_run_sorts_root_table_ascending(self: Self) -> None:
|
|
36
|
+
asc: Instruction
|
|
37
|
+
data: dict[str, Any]
|
|
38
|
+
path: Path
|
|
39
|
+
tmpdir: str
|
|
40
|
+
asc = get_instruction(False)
|
|
41
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
42
|
+
path = Path(tmpdir) / "example.json"
|
|
43
|
+
write_json(path, {"z": 1, "a": 2, "m": 3})
|
|
44
|
+
run(str(path), instructions=[asc])
|
|
45
|
+
data = read_json(path)
|
|
46
|
+
self.assertEqual(list(data.keys()), ["a", "m", "z"])
|
|
47
|
+
|
|
48
|
+
def test_run_sorts_root_table_descending(self: Self) -> None:
|
|
49
|
+
data: dict[str, Any]
|
|
50
|
+
desc: Instruction
|
|
51
|
+
path: Path
|
|
52
|
+
tmpdir: str
|
|
53
|
+
desc = get_instruction(True)
|
|
54
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
55
|
+
path = Path(tmpdir) / "example.json"
|
|
56
|
+
write_json(path, {"a": 1, "z": 2, "m": 3})
|
|
57
|
+
run(str(path), instructions=[desc])
|
|
58
|
+
data = read_json(path)
|
|
59
|
+
self.assertEqual(list(data.keys()), ["z", "m", "a"])
|
|
60
|
+
|
|
61
|
+
def test_run_sorts_nested_table_by_key_path(self: Self) -> None:
|
|
62
|
+
asc: Instruction
|
|
63
|
+
data: dict[str, Any]
|
|
64
|
+
nested: Any
|
|
65
|
+
path: Path
|
|
66
|
+
tmpdir: str
|
|
67
|
+
asc = get_instruction(False)
|
|
68
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
69
|
+
path = Path(tmpdir) / "example.json"
|
|
70
|
+
write_json(
|
|
71
|
+
path,
|
|
72
|
+
{
|
|
73
|
+
"tool": {
|
|
74
|
+
"json_sorted": {
|
|
75
|
+
"z": 1,
|
|
76
|
+
"a": 2,
|
|
77
|
+
"m": 3,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
run(
|
|
83
|
+
str(path),
|
|
84
|
+
instructions=[asc, "tool", "json_sorted"],
|
|
85
|
+
)
|
|
86
|
+
data = read_json(path)
|
|
87
|
+
nested = data["tool"]["json_sorted"]
|
|
88
|
+
self.assertEqual(list(nested.keys()), ["a", "m", "z"])
|
|
89
|
+
|
|
90
|
+
def test_run_sorts_nested_list_by_key_path(self: Self) -> None:
|
|
91
|
+
asc: Any
|
|
92
|
+
data: dict[str, Any]
|
|
93
|
+
path: Path
|
|
94
|
+
tmpdir: str
|
|
95
|
+
asc = get_instruction(False)
|
|
96
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
97
|
+
path = Path(tmpdir) / "example.json"
|
|
98
|
+
write_json(path, {"project": {"numbers": [3, 1, 2]}})
|
|
99
|
+
run(
|
|
100
|
+
str(path),
|
|
101
|
+
instructions=[asc, "project", "numbers"],
|
|
102
|
+
)
|
|
103
|
+
data = read_json(path)
|
|
104
|
+
self.assertEqual(data["project"]["numbers"], [1, 2, 3])
|
|
105
|
+
|
|
106
|
+
def test_run_accepts_glob_patterns(self: Self) -> None:
|
|
107
|
+
asc: Instruction
|
|
108
|
+
first: Path
|
|
109
|
+
root: Path
|
|
110
|
+
second: Path
|
|
111
|
+
tmpdir: str
|
|
112
|
+
asc = get_instruction(False)
|
|
113
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
114
|
+
root = Path(tmpdir)
|
|
115
|
+
first = root / "first.json"
|
|
116
|
+
second = root / "second.json"
|
|
117
|
+
write_json(first, {"b": 1, "a": 2})
|
|
118
|
+
write_json(second, {"d": 1, "c": 2})
|
|
119
|
+
run(str(root / "*.json"), instructions=[asc])
|
|
120
|
+
self.assertEqual(list(read_json(first).keys()), ["a", "b"])
|
|
121
|
+
self.assertEqual(list(read_json(second).keys()), ["c", "d"])
|
|
122
|
+
|
|
123
|
+
def test_run_ignores_duplicate_file_matches(self: Self) -> None:
|
|
124
|
+
asc: Instruction
|
|
125
|
+
data: dict[str, Any]
|
|
126
|
+
path: Path
|
|
127
|
+
tmpdir: str
|
|
128
|
+
asc = get_instruction(False)
|
|
129
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
130
|
+
path = Path(tmpdir) / "example.json"
|
|
131
|
+
write_json(path, {"b": 1, "a": 2})
|
|
132
|
+
run(str(path), str(path), instructions=[asc])
|
|
133
|
+
data = read_json(path)
|
|
134
|
+
self.assertEqual(list(data.keys()), ["a", "b"])
|
|
135
|
+
|
|
136
|
+
def test_run_raises_type_error_for_unsortable_value(self: Self) -> None:
|
|
137
|
+
asc: Instruction
|
|
138
|
+
path: Path
|
|
139
|
+
tmpdir: str
|
|
140
|
+
asc = get_instruction(False)
|
|
141
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
142
|
+
path = Path(tmpdir) / "example.json"
|
|
143
|
+
write_json(path, {"project": {"name": "demo"}})
|
|
144
|
+
with self.assertRaises(TypeError):
|
|
145
|
+
run(
|
|
146
|
+
str(path),
|
|
147
|
+
instructions=[asc, "project", "name"],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
unittest.main()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
import unittest
|
|
4
|
+
from io import BufferedReader, BufferedWriter
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Self
|
|
7
|
+
|
|
8
|
+
from json_sorted.core.run import run, run_instruction_on_data
|
|
9
|
+
from json_sorted.enum.Instruction import Instruction
|
|
10
|
+
from json_sorted.enum.Selector import Selector
|
|
11
|
+
|
|
12
|
+
__all__ = ["TestAllKeys"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def write_json(path: Path, data: dict[str, Any]) -> None:
|
|
16
|
+
stream: Any
|
|
17
|
+
with path.open("w") as stream:
|
|
18
|
+
json.dump(data, stream)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def read_json(path: Path) -> Any:
|
|
22
|
+
stream: Any
|
|
23
|
+
with path.open("r") as stream:
|
|
24
|
+
return json.load(stream)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestAllKeys(unittest.TestCase):
|
|
28
|
+
def test_all_keys_documented_example(self: Self) -> None:
|
|
29
|
+
# --sort --key=foo --all-keys --index=0
|
|
30
|
+
data: dict[str, Any]
|
|
31
|
+
result: dict[str, Any]
|
|
32
|
+
data = {
|
|
33
|
+
"foo": {
|
|
34
|
+
"bar": [[4, 2], {}],
|
|
35
|
+
"baz": [{"a": 9, "c": 8, "b": 7}],
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
result = run_instruction_on_data(
|
|
39
|
+
instruction=Instruction.SORT,
|
|
40
|
+
keys=["foo", Selector.ALL_KEYS, 0],
|
|
41
|
+
data=data,
|
|
42
|
+
)
|
|
43
|
+
self.assertEqual(
|
|
44
|
+
result,
|
|
45
|
+
{
|
|
46
|
+
"foo": {
|
|
47
|
+
"bar": [[2, 4], {}],
|
|
48
|
+
"baz": [{"a": 9, "b": 7, "c": 8}],
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def test_all_keys_expands_every_child_table(self: Self) -> None:
|
|
54
|
+
data: dict[str, Any]
|
|
55
|
+
path: Path
|
|
56
|
+
tmpdir: str
|
|
57
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
58
|
+
path = Path(tmpdir) / "example.json"
|
|
59
|
+
write_json(
|
|
60
|
+
path,
|
|
61
|
+
{
|
|
62
|
+
"tool": {
|
|
63
|
+
"x": {"c": 1, "a": 2, "b": 3},
|
|
64
|
+
"y": {"z": 1, "m": 2},
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
# --sort --key=tool --all-keys
|
|
69
|
+
run(
|
|
70
|
+
str(path),
|
|
71
|
+
instructions=[Instruction.SORT, "tool", Selector.ALL_KEYS],
|
|
72
|
+
)
|
|
73
|
+
data = read_json(path)
|
|
74
|
+
self.assertEqual(list(data["tool"]["x"].keys()), ["a", "b", "c"])
|
|
75
|
+
self.assertEqual(list(data["tool"]["y"].keys()), ["m", "z"])
|
|
76
|
+
|
|
77
|
+
def test_all_keys_expands_list_elements(self: Self) -> None:
|
|
78
|
+
data: dict[str, Any]
|
|
79
|
+
path: Path
|
|
80
|
+
tmpdir: str
|
|
81
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
82
|
+
path = Path(tmpdir) / "example.json"
|
|
83
|
+
write_json(path, {"rows": [[3, 1, 2], [9, 8, 7]]})
|
|
84
|
+
# --sort --key=rows --all-keys
|
|
85
|
+
with self.assertRaises(Exception):
|
|
86
|
+
run(
|
|
87
|
+
str(path),
|
|
88
|
+
instructions=[Instruction.SORT, "rows", Selector.ALL_KEYS],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def test_all_indices_expands_list_elements(self: Self) -> None:
|
|
92
|
+
data: dict[str, Any]
|
|
93
|
+
path: Path
|
|
94
|
+
tmpdir: str
|
|
95
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
96
|
+
path = Path(tmpdir) / "example.json"
|
|
97
|
+
write_json(path, {"rows": [[3, 1, 2], [9, 8, 7]]})
|
|
98
|
+
# --sort --key=rows --all-indices
|
|
99
|
+
run(
|
|
100
|
+
str(path),
|
|
101
|
+
instructions=[Instruction.SORT, "rows", Selector.ALL_INDICES],
|
|
102
|
+
)
|
|
103
|
+
data = read_json(path)
|
|
104
|
+
self.assertEqual(data["rows"], [[1, 2, 3], [7, 8, 9]])
|
|
105
|
+
|
|
106
|
+
def test_all_keys_respects_sort_reverse(self: Self) -> None:
|
|
107
|
+
data: dict[str, Any]
|
|
108
|
+
result: dict[str, Any]
|
|
109
|
+
data = {"foo": {"a": [3, 1, 2], "b": [6, 5, 4]}}
|
|
110
|
+
result = run_instruction_on_data(
|
|
111
|
+
instruction=Instruction.SORT_REVERSE,
|
|
112
|
+
keys=["foo", Selector.ALL_KEYS],
|
|
113
|
+
data=data,
|
|
114
|
+
)
|
|
115
|
+
self.assertEqual(result, {"foo": {"a": [3, 2, 1], "b": [6, 5, 4]}})
|
|
116
|
+
|
|
117
|
+
def test_all_keys_on_scalar_raises_type_error(self: Self) -> None:
|
|
118
|
+
with self.assertRaises(TypeError):
|
|
119
|
+
run_instruction_on_data(
|
|
120
|
+
instruction=Instruction.SORT,
|
|
121
|
+
keys=[Selector.ALL_KEYS],
|
|
122
|
+
data={"name": "demo"}["name"], # type: ignore[arg-type]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
unittest.main()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Self
|
|
6
|
+
|
|
7
|
+
from json_sorted.core.main import main
|
|
8
|
+
|
|
9
|
+
__all__ = ["TestCliAllIndices"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCliAllIndices(unittest.TestCase):
|
|
13
|
+
def test_cli_accepts_all_indices_selector(self: Self) -> None:
|
|
14
|
+
data: dict[str, Any]
|
|
15
|
+
dataA: dict[str, Any]
|
|
16
|
+
listA: list[str]
|
|
17
|
+
path: Path
|
|
18
|
+
stream: Any
|
|
19
|
+
tmpdir: str
|
|
20
|
+
dataA = {
|
|
21
|
+
"items": [
|
|
22
|
+
{"b": 2, "a": 1},
|
|
23
|
+
{"d": 4, "c": 3},
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
listA = [
|
|
27
|
+
"--sort",
|
|
28
|
+
"--key",
|
|
29
|
+
"items",
|
|
30
|
+
"--all-indices",
|
|
31
|
+
]
|
|
32
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
33
|
+
path = Path(tmpdir) / "example.json"
|
|
34
|
+
with path.open("w") as stream:
|
|
35
|
+
json.dump(dataA, stream)
|
|
36
|
+
main(listA + [str(path)])
|
|
37
|
+
with path.open("r") as stream:
|
|
38
|
+
data = json.load(stream)
|
|
39
|
+
self.assertEqual(list(data["items"][0].keys()), ["a", "b"])
|
|
40
|
+
self.assertEqual(list(data["items"][1].keys()), ["c", "d"])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
unittest.main()
|