keytidy 0.1.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.
- keytidy-0.1.0/LICENSE +21 -0
- keytidy-0.1.0/PKG-INFO +128 -0
- keytidy-0.1.0/README.md +105 -0
- keytidy-0.1.0/pyproject.toml +38 -0
- keytidy-0.1.0/setup.cfg +4 -0
- keytidy-0.1.0/src/keytidy/__init__.py +3 -0
- keytidy-0.1.0/src/keytidy/__main__.py +6 -0
- keytidy-0.1.0/src/keytidy/cli.py +164 -0
- keytidy-0.1.0/src/keytidy/core.py +96 -0
- keytidy-0.1.0/src/keytidy.egg-info/PKG-INFO +128 -0
- keytidy-0.1.0/src/keytidy.egg-info/SOURCES.txt +13 -0
- keytidy-0.1.0/src/keytidy.egg-info/dependency_links.txt +1 -0
- keytidy-0.1.0/src/keytidy.egg-info/entry_points.txt +2 -0
- keytidy-0.1.0/src/keytidy.egg-info/top_level.txt +1 -0
- keytidy-0.1.0/tests/test_core.py +103 -0
keytidy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 keytidy contributors
|
|
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.
|
keytidy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keytidy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tidy JSON key order — and sort package.json the conventional way (canonical field order, sorted dependencies, untouched scripts). Zero dependencies.
|
|
5
|
+
Author: yyfjj
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jjdoor/keytidy-py
|
|
8
|
+
Project-URL: Repository, https://github.com/jjdoor/keytidy-py
|
|
9
|
+
Project-URL: Issues, https://github.com/jjdoor/keytidy-py/issues
|
|
10
|
+
Keywords: json,package.json,sort,format,formatter,tidy,keys,cli,pre-commit
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# keytidy
|
|
25
|
+
|
|
26
|
+
**Tidy JSON key order — and sort `package.json` the way it's actually meant to
|
|
27
|
+
look.** Inconsistent key order is pure diff noise: one teammate's editor writes
|
|
28
|
+
`name` first, another's tool writes it last, and every PR churns lines that
|
|
29
|
+
didn't really change. keytidy gives every JSON file one deterministic order.
|
|
30
|
+
**Zero dependencies** (pure standard library).
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install keytidy
|
|
34
|
+
|
|
35
|
+
keytidy # sort ./package.json in place
|
|
36
|
+
keytidy --check # CI gate: exit 1 if anything isn't sorted
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## It's not just "sort all the keys"
|
|
40
|
+
|
|
41
|
+
Sorting `package.json` alphabetically would be *wrong* — `name` belongs at the
|
|
42
|
+
top, not wedged between `module` and `optionalDependencies`. So keytidy treats
|
|
43
|
+
`package.json` specially:
|
|
44
|
+
|
|
45
|
+
- **Top-level fields** follow the conventional order (`name`, `version`,
|
|
46
|
+
`description`, …, `scripts`, `dependencies`, `devDependencies`, …). Unknown
|
|
47
|
+
fields (your `tool.*`, custom keys) sort alphabetically after the known ones.
|
|
48
|
+
- **Dependency blocks** (`dependencies`, `devDependencies`, `peerDependencies`,
|
|
49
|
+
…) are sorted A→Z — the part you actually want sorted.
|
|
50
|
+
- **`scripts` is left in your order**, because script order is frequently
|
|
51
|
+
meaningful (`pretest` → `test` → `posttest`, ordered build steps). Opt in with
|
|
52
|
+
`--sort-scripts` if you disagree.
|
|
53
|
+
|
|
54
|
+
Every other `.json` file just gets a clean recursive alphabetical sort, with
|
|
55
|
+
**arrays left in their original order** (an array is data, not config).
|
|
56
|
+
|
|
57
|
+
## Example
|
|
58
|
+
|
|
59
|
+
```jsonc
|
|
60
|
+
// before
|
|
61
|
+
{ "version": "1.0.0", "scripts": { "test": "…", "build": "…" },
|
|
62
|
+
"name": "demo", "dependencies": { "zod": "…", "axios": "…" } }
|
|
63
|
+
|
|
64
|
+
// after → keytidy
|
|
65
|
+
{
|
|
66
|
+
"name": "demo",
|
|
67
|
+
"version": "1.0.0",
|
|
68
|
+
"scripts": { "test": "…", "build": "…" }, // order preserved
|
|
69
|
+
"dependencies": { "axios": "…", "zod": "…" } // sorted A→Z
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
keytidy # sort ./package.json in place
|
|
77
|
+
keytidy package.json tsconfig.json
|
|
78
|
+
keytidy *.json # your shell expands the glob
|
|
79
|
+
keytidy --check # don't write; exit 1 if any file isn't sorted
|
|
80
|
+
keytidy --stdout pkg.json # print the sorted result instead of writing
|
|
81
|
+
keytidy --indent 4 # force 4-space indent (default: detected, else 2)
|
|
82
|
+
keytidy --sort-scripts # also sort the package.json scripts block
|
|
83
|
+
keytidy --all-keys # ignore the conventional order, sort A→Z
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
You can also run it as a module: `python -m keytidy`.
|
|
87
|
+
|
|
88
|
+
Indentation is **detected from the file** and preserved, so keytidy doesn't
|
|
89
|
+
fight your existing 2-space / 4-space / tab style. The trailing newline is kept
|
|
90
|
+
as-is.
|
|
91
|
+
|
|
92
|
+
### As a CI / pre-commit gate
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
# .pre-commit-config.yaml
|
|
96
|
+
- repo: local
|
|
97
|
+
hooks:
|
|
98
|
+
- id: keytidy
|
|
99
|
+
name: keytidy
|
|
100
|
+
entry: keytidy --check
|
|
101
|
+
language: system
|
|
102
|
+
files: package\.json$
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Exit codes
|
|
106
|
+
|
|
107
|
+
| Code | Meaning |
|
|
108
|
+
|------|---------|
|
|
109
|
+
| `0` | sorted (write mode) or everything already sorted (`--check`) |
|
|
110
|
+
| `1` | a file needs sorting (`--check` only) |
|
|
111
|
+
| `2` | invalid JSON, or a file couldn't be read/written |
|
|
112
|
+
|
|
113
|
+
## Notes
|
|
114
|
+
|
|
115
|
+
- It rewrites by parsing and re-serializing, so it normalizes JSON formatting.
|
|
116
|
+
It does **not** support comments or trailing commas (JSONC / `tsconfig.json`
|
|
117
|
+
with comments will report invalid JSON rather than mangle them).
|
|
118
|
+
- The sort itself is pure and deterministic, shared with the Node port — for
|
|
119
|
+
typical config files the two produce identical output.
|
|
120
|
+
|
|
121
|
+
## Also available for Node
|
|
122
|
+
|
|
123
|
+
Same behavior, same flags: [`npx keytidy`](https://www.npmjs.com/package/keytidy)
|
|
124
|
+
(source: [keytidy](https://github.com/jjdoor/keytidy)).
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
keytidy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# keytidy
|
|
2
|
+
|
|
3
|
+
**Tidy JSON key order — and sort `package.json` the way it's actually meant to
|
|
4
|
+
look.** Inconsistent key order is pure diff noise: one teammate's editor writes
|
|
5
|
+
`name` first, another's tool writes it last, and every PR churns lines that
|
|
6
|
+
didn't really change. keytidy gives every JSON file one deterministic order.
|
|
7
|
+
**Zero dependencies** (pure standard library).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install keytidy
|
|
11
|
+
|
|
12
|
+
keytidy # sort ./package.json in place
|
|
13
|
+
keytidy --check # CI gate: exit 1 if anything isn't sorted
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## It's not just "sort all the keys"
|
|
17
|
+
|
|
18
|
+
Sorting `package.json` alphabetically would be *wrong* — `name` belongs at the
|
|
19
|
+
top, not wedged between `module` and `optionalDependencies`. So keytidy treats
|
|
20
|
+
`package.json` specially:
|
|
21
|
+
|
|
22
|
+
- **Top-level fields** follow the conventional order (`name`, `version`,
|
|
23
|
+
`description`, …, `scripts`, `dependencies`, `devDependencies`, …). Unknown
|
|
24
|
+
fields (your `tool.*`, custom keys) sort alphabetically after the known ones.
|
|
25
|
+
- **Dependency blocks** (`dependencies`, `devDependencies`, `peerDependencies`,
|
|
26
|
+
…) are sorted A→Z — the part you actually want sorted.
|
|
27
|
+
- **`scripts` is left in your order**, because script order is frequently
|
|
28
|
+
meaningful (`pretest` → `test` → `posttest`, ordered build steps). Opt in with
|
|
29
|
+
`--sort-scripts` if you disagree.
|
|
30
|
+
|
|
31
|
+
Every other `.json` file just gets a clean recursive alphabetical sort, with
|
|
32
|
+
**arrays left in their original order** (an array is data, not config).
|
|
33
|
+
|
|
34
|
+
## Example
|
|
35
|
+
|
|
36
|
+
```jsonc
|
|
37
|
+
// before
|
|
38
|
+
{ "version": "1.0.0", "scripts": { "test": "…", "build": "…" },
|
|
39
|
+
"name": "demo", "dependencies": { "zod": "…", "axios": "…" } }
|
|
40
|
+
|
|
41
|
+
// after → keytidy
|
|
42
|
+
{
|
|
43
|
+
"name": "demo",
|
|
44
|
+
"version": "1.0.0",
|
|
45
|
+
"scripts": { "test": "…", "build": "…" }, // order preserved
|
|
46
|
+
"dependencies": { "axios": "…", "zod": "…" } // sorted A→Z
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
keytidy # sort ./package.json in place
|
|
54
|
+
keytidy package.json tsconfig.json
|
|
55
|
+
keytidy *.json # your shell expands the glob
|
|
56
|
+
keytidy --check # don't write; exit 1 if any file isn't sorted
|
|
57
|
+
keytidy --stdout pkg.json # print the sorted result instead of writing
|
|
58
|
+
keytidy --indent 4 # force 4-space indent (default: detected, else 2)
|
|
59
|
+
keytidy --sort-scripts # also sort the package.json scripts block
|
|
60
|
+
keytidy --all-keys # ignore the conventional order, sort A→Z
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
You can also run it as a module: `python -m keytidy`.
|
|
64
|
+
|
|
65
|
+
Indentation is **detected from the file** and preserved, so keytidy doesn't
|
|
66
|
+
fight your existing 2-space / 4-space / tab style. The trailing newline is kept
|
|
67
|
+
as-is.
|
|
68
|
+
|
|
69
|
+
### As a CI / pre-commit gate
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
# .pre-commit-config.yaml
|
|
73
|
+
- repo: local
|
|
74
|
+
hooks:
|
|
75
|
+
- id: keytidy
|
|
76
|
+
name: keytidy
|
|
77
|
+
entry: keytidy --check
|
|
78
|
+
language: system
|
|
79
|
+
files: package\.json$
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Exit codes
|
|
83
|
+
|
|
84
|
+
| Code | Meaning |
|
|
85
|
+
|------|---------|
|
|
86
|
+
| `0` | sorted (write mode) or everything already sorted (`--check`) |
|
|
87
|
+
| `1` | a file needs sorting (`--check` only) |
|
|
88
|
+
| `2` | invalid JSON, or a file couldn't be read/written |
|
|
89
|
+
|
|
90
|
+
## Notes
|
|
91
|
+
|
|
92
|
+
- It rewrites by parsing and re-serializing, so it normalizes JSON formatting.
|
|
93
|
+
It does **not** support comments or trailing commas (JSONC / `tsconfig.json`
|
|
94
|
+
with comments will report invalid JSON rather than mangle them).
|
|
95
|
+
- The sort itself is pure and deterministic, shared with the Node port — for
|
|
96
|
+
typical config files the two produce identical output.
|
|
97
|
+
|
|
98
|
+
## Also available for Node
|
|
99
|
+
|
|
100
|
+
Same behavior, same flags: [`npx keytidy`](https://www.npmjs.com/package/keytidy)
|
|
101
|
+
(source: [keytidy](https://github.com/jjdoor/keytidy)).
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "keytidy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Tidy JSON key order — and sort package.json the conventional way (canonical field order, sorted dependencies, untouched scripts). Zero dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "yyfjj" }]
|
|
13
|
+
keywords = ["json", "package.json", "sort", "format", "formatter", "tidy", "keys", "cli", "pre-commit"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
22
|
+
"Topic :: Utilities",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/jjdoor/keytidy-py"
|
|
28
|
+
Repository = "https://github.com/jjdoor/keytidy-py"
|
|
29
|
+
Issues = "https://github.com/jjdoor/keytidy-py/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
keytidy = "keytidy.cli:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
package-dir = { "" = "src" }
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
keytidy-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""keytidy command-line interface."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import core
|
|
8
|
+
|
|
9
|
+
VERSION = "0.1.0"
|
|
10
|
+
|
|
11
|
+
# ---- tiny color helpers (no dep) ----
|
|
12
|
+
_COLOR = sys.stdout.isatty() and not os.environ.get("NO_COLOR")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _c(code, s):
|
|
16
|
+
return f"\x1b[{code}m{s}\x1b[0m" if _COLOR else s
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def red(s): return _c("31", s)
|
|
20
|
+
def green(s): return _c("32", s)
|
|
21
|
+
def yellow(s): return _c("33", s)
|
|
22
|
+
def dim(s): return _c("2", s)
|
|
23
|
+
def bold(s): return _c("1", s)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
HELP = f"""{bold('keytidy')} — tidy JSON key order (and package.json the way it's meant to look).
|
|
27
|
+
|
|
28
|
+
{bold('Usage')}
|
|
29
|
+
keytidy [file...] Sort & rewrite the files (defaults to ./package.json)
|
|
30
|
+
keytidy --check Don't write; exit 1 if anything isn't sorted (CI)
|
|
31
|
+
keytidy --stdout Print the sorted result instead of writing
|
|
32
|
+
keytidy *.json Sort every JSON file (your shell expands the glob)
|
|
33
|
+
|
|
34
|
+
{bold('Options')}
|
|
35
|
+
--indent <n|tab> Override indentation (default: detected from the file, else 2)
|
|
36
|
+
--sort-scripts Also sort package.json "scripts" (off — script order matters)
|
|
37
|
+
--all-keys Sort package.json purely alphabetically (ignore the conventional order)
|
|
38
|
+
--version
|
|
39
|
+
|
|
40
|
+
{bold('What it does')}
|
|
41
|
+
• generic JSON — every object's keys sorted A->Z; arrays kept in order
|
|
42
|
+
• package.json — conventional field order (name, version, …, dependencies),
|
|
43
|
+
dependency blocks sorted A->Z, "scripts" left as you wrote it
|
|
44
|
+
|
|
45
|
+
{bold('Exit')} 0 sorted/clean · 1 needs sorting (--check) · 2 bad JSON / IO error
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def die(msg):
|
|
50
|
+
sys.stderr.write(red(f"keytidy: {msg}\n"))
|
|
51
|
+
sys.exit(2)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_args(argv):
|
|
55
|
+
opts = {"files": [], "check": False, "stdout": False, "indent": None,
|
|
56
|
+
"sortScripts": False, "allKeys": False}
|
|
57
|
+
i = 0
|
|
58
|
+
while i < len(argv):
|
|
59
|
+
a = argv[i]
|
|
60
|
+
if a == "--check":
|
|
61
|
+
opts["check"] = True
|
|
62
|
+
elif a == "--stdout":
|
|
63
|
+
opts["stdout"] = True
|
|
64
|
+
elif a == "--sort-scripts":
|
|
65
|
+
opts["sortScripts"] = True
|
|
66
|
+
elif a == "--all-keys":
|
|
67
|
+
opts["allKeys"] = True
|
|
68
|
+
elif a == "--indent":
|
|
69
|
+
i += 1
|
|
70
|
+
if i >= len(argv):
|
|
71
|
+
die('--indent needs a value (a number or "tab")')
|
|
72
|
+
v = argv[i]
|
|
73
|
+
if v == "tab":
|
|
74
|
+
opts["indent"] = "\t"
|
|
75
|
+
elif v.isdigit():
|
|
76
|
+
opts["indent"] = int(v)
|
|
77
|
+
else:
|
|
78
|
+
die('--indent must be a number or "tab"')
|
|
79
|
+
elif a.startswith("-"):
|
|
80
|
+
die(f"unknown flag: {a} (try --help)")
|
|
81
|
+
else:
|
|
82
|
+
opts["files"].append(a)
|
|
83
|
+
i += 1
|
|
84
|
+
return opts
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def resolve_files(files):
|
|
88
|
+
if files:
|
|
89
|
+
return files
|
|
90
|
+
if os.path.exists("package.json"):
|
|
91
|
+
return ["package.json"]
|
|
92
|
+
die("no files given and no ./package.json found — pass a JSON file")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main(argv=None):
|
|
96
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
97
|
+
if "-h" in argv or "--help" in argv:
|
|
98
|
+
sys.stdout.write(HELP)
|
|
99
|
+
return 0
|
|
100
|
+
if "-v" in argv or "--version" in argv:
|
|
101
|
+
sys.stdout.write(VERSION + "\n")
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
opts = parse_args(argv)
|
|
105
|
+
files = resolve_files(opts["files"])
|
|
106
|
+
|
|
107
|
+
errored = False
|
|
108
|
+
needs_sort = False
|
|
109
|
+
|
|
110
|
+
for file in files:
|
|
111
|
+
try:
|
|
112
|
+
with open(file, encoding="utf-8") as f:
|
|
113
|
+
text = f.read()
|
|
114
|
+
except FileNotFoundError:
|
|
115
|
+
sys.stderr.write(red(f"✗ {file} — no such file\n"))
|
|
116
|
+
errored = True
|
|
117
|
+
continue
|
|
118
|
+
except OSError as e:
|
|
119
|
+
sys.stderr.write(red(f"✗ {file} — {e}\n"))
|
|
120
|
+
errored = True
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
result = core.process(text, {
|
|
125
|
+
"packageJson": os.path.basename(file) == "package.json",
|
|
126
|
+
"indent": opts["indent"],
|
|
127
|
+
"sortScripts": opts["sortScripts"],
|
|
128
|
+
"allKeys": opts["allKeys"],
|
|
129
|
+
})
|
|
130
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
131
|
+
sys.stderr.write(red(f"✗ {file} — invalid JSON: {e}\n"))
|
|
132
|
+
errored = True
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
if opts["stdout"]:
|
|
136
|
+
sys.stdout.write(result["output"])
|
|
137
|
+
continue
|
|
138
|
+
if opts["check"]:
|
|
139
|
+
if result["changed"]:
|
|
140
|
+
needs_sort = True
|
|
141
|
+
sys.stdout.write("{} {} {}\n".format(yellow("✗"), file, dim("— not sorted")))
|
|
142
|
+
else:
|
|
143
|
+
sys.stdout.write("{} {} {}\n".format(green("✓"), file, dim("— sorted")))
|
|
144
|
+
continue
|
|
145
|
+
# write mode
|
|
146
|
+
if result["changed"]:
|
|
147
|
+
try:
|
|
148
|
+
with open(file, "w", encoding="utf-8") as f:
|
|
149
|
+
f.write(result["output"])
|
|
150
|
+
except OSError as e:
|
|
151
|
+
sys.stderr.write(red(f"✗ {file} — {e}\n"))
|
|
152
|
+
errored = True
|
|
153
|
+
continue
|
|
154
|
+
sys.stdout.write("{} {} {}\n".format(green("✓"), bold(file), dim("— sorted")))
|
|
155
|
+
else:
|
|
156
|
+
sys.stdout.write("{} {} {}\n".format(dim("•"), file, dim("— already sorted")))
|
|
157
|
+
|
|
158
|
+
if errored:
|
|
159
|
+
return 2
|
|
160
|
+
if opts["check"] and needs_sort:
|
|
161
|
+
if not opts["stdout"]:
|
|
162
|
+
sys.stdout.write("\n{} {} {}\n".format(dim("run"), bold("keytidy"), dim("to fix")))
|
|
163
|
+
return 1
|
|
164
|
+
return 0
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""keytidy core — pure JSON key-ordering logic. No fs, no process, no clock.
|
|
2
|
+
|
|
3
|
+
Two jobs:
|
|
4
|
+
|
|
5
|
+
1. Generic JSON — recursively sort every object's keys alphabetically. Arrays
|
|
6
|
+
keep their order (arrays are data, not config); primitives are untouched.
|
|
7
|
+
The point is to kill the diff noise from tools that emit keys in arbitrary
|
|
8
|
+
order.
|
|
9
|
+
2. package.json — sorting it alphabetically would be *wrong* (``name`` should
|
|
10
|
+
come before ``dependencies``). So package.json gets the conventional field
|
|
11
|
+
order at the top level, its dependency objects sorted alphabetically, and
|
|
12
|
+
its ``scripts`` block left in the author's order.
|
|
13
|
+
|
|
14
|
+
Mirrors the Node port behavior. The fs/CLI plumbing lives in cli.py.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
# Conventional top-level order for package.json (npm's own ordering plus what
|
|
21
|
+
# the ecosystem settled on). Keys not in this list are appended alphabetically.
|
|
22
|
+
PACKAGE_FIELD_ORDER = [
|
|
23
|
+
"$schema", "name", "displayName", "version", "private", "description",
|
|
24
|
+
"keywords", "license", "licenses", "homepage", "repository", "bugs", "funding",
|
|
25
|
+
"author", "contributors", "maintainers", "type", "imports", "exports", "main",
|
|
26
|
+
"module", "browser", "unpkg", "jsdelivr", "types", "typings", "typesVersions",
|
|
27
|
+
"bin", "man", "files", "directories", "scripts", "config", "sideEffects",
|
|
28
|
+
"workspaces", "engines", "engineStrict", "os", "cpu", "packageManager",
|
|
29
|
+
"publishConfig", "dependencies", "devDependencies", "peerDependencies",
|
|
30
|
+
"peerDependenciesMeta", "optionalDependencies", "bundledDependencies",
|
|
31
|
+
"bundleDependencies", "overrides", "resolutions",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# Within package.json, these object values keep their original key order.
|
|
35
|
+
PRESERVE_ORDER_FIELDS = {"scripts"}
|
|
36
|
+
|
|
37
|
+
_INDENT_RE = re.compile(r'\n([ \t]+)["\]{}]')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def detect_indent(text):
|
|
41
|
+
"""Detect the indentation of an existing JSON document. Returns a space
|
|
42
|
+
count, the string "\\t", or 2."""
|
|
43
|
+
m = _INDENT_RE.search(text)
|
|
44
|
+
if not m:
|
|
45
|
+
return 2
|
|
46
|
+
return "\t" if m.group(1)[0] == "\t" else len(m.group(1))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def order_package_keys(keys):
|
|
50
|
+
"""Return package.json keys in conventional order: known fields first (in
|
|
51
|
+
canonical sequence), then unknown fields alphabetically."""
|
|
52
|
+
known = set(PACKAGE_FIELD_ORDER)
|
|
53
|
+
present = [k for k in PACKAGE_FIELD_ORDER if k in keys]
|
|
54
|
+
unknown = sorted(k for k in keys if k not in known)
|
|
55
|
+
return present + unknown
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def sort_value(value, opts, package_top):
|
|
59
|
+
"""Recursively produce a key-sorted copy of ``value``."""
|
|
60
|
+
if isinstance(value, list):
|
|
61
|
+
return [sort_value(v, opts, False) for v in value]
|
|
62
|
+
if not isinstance(value, dict):
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
keys = list(value.keys())
|
|
66
|
+
if package_top and not opts.get("allKeys"):
|
|
67
|
+
ordered = order_package_keys(keys)
|
|
68
|
+
else:
|
|
69
|
+
ordered = sorted(keys)
|
|
70
|
+
|
|
71
|
+
out = {}
|
|
72
|
+
for k in ordered:
|
|
73
|
+
preserve = package_top and not opts.get("sortScripts") and k in PRESERVE_ORDER_FIELDS
|
|
74
|
+
out[k] = value[k] if preserve else sort_value(value[k], opts, False)
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def serialize(value, indent):
|
|
79
|
+
"""Serialize matching the Node port: given indent, ``: `` / ``,`` separators,
|
|
80
|
+
UTF-8 (no \\uXXXX escaping)."""
|
|
81
|
+
return json.dumps(value, indent=indent, ensure_ascii=False, separators=(",", ": "))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def process(text, opts=None):
|
|
85
|
+
"""Sort one JSON document. Returns ``{"output": str, "changed": bool}``.
|
|
86
|
+
Raises ValueError (via json) on invalid JSON."""
|
|
87
|
+
opts = opts or {}
|
|
88
|
+
data = json.loads(text)
|
|
89
|
+
indent = opts["indent"] if opts.get("indent") is not None else detect_indent(text)
|
|
90
|
+
package_top = bool(opts.get("packageJson")) and isinstance(data, dict)
|
|
91
|
+
sorted_data = sort_value(data, {"sortScripts": bool(opts.get("sortScripts")),
|
|
92
|
+
"allKeys": bool(opts.get("allKeys"))}, package_top)
|
|
93
|
+
output = serialize(sorted_data, indent)
|
|
94
|
+
if text.endswith("\n"):
|
|
95
|
+
output += "\n"
|
|
96
|
+
return {"output": output, "changed": output != text}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keytidy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tidy JSON key order — and sort package.json the conventional way (canonical field order, sorted dependencies, untouched scripts). Zero dependencies.
|
|
5
|
+
Author: yyfjj
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jjdoor/keytidy-py
|
|
8
|
+
Project-URL: Repository, https://github.com/jjdoor/keytidy-py
|
|
9
|
+
Project-URL: Issues, https://github.com/jjdoor/keytidy-py/issues
|
|
10
|
+
Keywords: json,package.json,sort,format,formatter,tidy,keys,cli,pre-commit
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# keytidy
|
|
25
|
+
|
|
26
|
+
**Tidy JSON key order — and sort `package.json` the way it's actually meant to
|
|
27
|
+
look.** Inconsistent key order is pure diff noise: one teammate's editor writes
|
|
28
|
+
`name` first, another's tool writes it last, and every PR churns lines that
|
|
29
|
+
didn't really change. keytidy gives every JSON file one deterministic order.
|
|
30
|
+
**Zero dependencies** (pure standard library).
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install keytidy
|
|
34
|
+
|
|
35
|
+
keytidy # sort ./package.json in place
|
|
36
|
+
keytidy --check # CI gate: exit 1 if anything isn't sorted
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## It's not just "sort all the keys"
|
|
40
|
+
|
|
41
|
+
Sorting `package.json` alphabetically would be *wrong* — `name` belongs at the
|
|
42
|
+
top, not wedged between `module` and `optionalDependencies`. So keytidy treats
|
|
43
|
+
`package.json` specially:
|
|
44
|
+
|
|
45
|
+
- **Top-level fields** follow the conventional order (`name`, `version`,
|
|
46
|
+
`description`, …, `scripts`, `dependencies`, `devDependencies`, …). Unknown
|
|
47
|
+
fields (your `tool.*`, custom keys) sort alphabetically after the known ones.
|
|
48
|
+
- **Dependency blocks** (`dependencies`, `devDependencies`, `peerDependencies`,
|
|
49
|
+
…) are sorted A→Z — the part you actually want sorted.
|
|
50
|
+
- **`scripts` is left in your order**, because script order is frequently
|
|
51
|
+
meaningful (`pretest` → `test` → `posttest`, ordered build steps). Opt in with
|
|
52
|
+
`--sort-scripts` if you disagree.
|
|
53
|
+
|
|
54
|
+
Every other `.json` file just gets a clean recursive alphabetical sort, with
|
|
55
|
+
**arrays left in their original order** (an array is data, not config).
|
|
56
|
+
|
|
57
|
+
## Example
|
|
58
|
+
|
|
59
|
+
```jsonc
|
|
60
|
+
// before
|
|
61
|
+
{ "version": "1.0.0", "scripts": { "test": "…", "build": "…" },
|
|
62
|
+
"name": "demo", "dependencies": { "zod": "…", "axios": "…" } }
|
|
63
|
+
|
|
64
|
+
// after → keytidy
|
|
65
|
+
{
|
|
66
|
+
"name": "demo",
|
|
67
|
+
"version": "1.0.0",
|
|
68
|
+
"scripts": { "test": "…", "build": "…" }, // order preserved
|
|
69
|
+
"dependencies": { "axios": "…", "zod": "…" } // sorted A→Z
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
keytidy # sort ./package.json in place
|
|
77
|
+
keytidy package.json tsconfig.json
|
|
78
|
+
keytidy *.json # your shell expands the glob
|
|
79
|
+
keytidy --check # don't write; exit 1 if any file isn't sorted
|
|
80
|
+
keytidy --stdout pkg.json # print the sorted result instead of writing
|
|
81
|
+
keytidy --indent 4 # force 4-space indent (default: detected, else 2)
|
|
82
|
+
keytidy --sort-scripts # also sort the package.json scripts block
|
|
83
|
+
keytidy --all-keys # ignore the conventional order, sort A→Z
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
You can also run it as a module: `python -m keytidy`.
|
|
87
|
+
|
|
88
|
+
Indentation is **detected from the file** and preserved, so keytidy doesn't
|
|
89
|
+
fight your existing 2-space / 4-space / tab style. The trailing newline is kept
|
|
90
|
+
as-is.
|
|
91
|
+
|
|
92
|
+
### As a CI / pre-commit gate
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
# .pre-commit-config.yaml
|
|
96
|
+
- repo: local
|
|
97
|
+
hooks:
|
|
98
|
+
- id: keytidy
|
|
99
|
+
name: keytidy
|
|
100
|
+
entry: keytidy --check
|
|
101
|
+
language: system
|
|
102
|
+
files: package\.json$
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Exit codes
|
|
106
|
+
|
|
107
|
+
| Code | Meaning |
|
|
108
|
+
|------|---------|
|
|
109
|
+
| `0` | sorted (write mode) or everything already sorted (`--check`) |
|
|
110
|
+
| `1` | a file needs sorting (`--check` only) |
|
|
111
|
+
| `2` | invalid JSON, or a file couldn't be read/written |
|
|
112
|
+
|
|
113
|
+
## Notes
|
|
114
|
+
|
|
115
|
+
- It rewrites by parsing and re-serializing, so it normalizes JSON formatting.
|
|
116
|
+
It does **not** support comments or trailing commas (JSONC / `tsconfig.json`
|
|
117
|
+
with comments will report invalid JSON rather than mangle them).
|
|
118
|
+
- The sort itself is pure and deterministic, shared with the Node port — for
|
|
119
|
+
typical config files the two produce identical output.
|
|
120
|
+
|
|
121
|
+
## Also available for Node
|
|
122
|
+
|
|
123
|
+
Same behavior, same flags: [`npx keytidy`](https://www.npmjs.com/package/keytidy)
|
|
124
|
+
(source: [keytidy](https://github.com/jjdoor/keytidy)).
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/keytidy/__init__.py
|
|
5
|
+
src/keytidy/__main__.py
|
|
6
|
+
src/keytidy/cli.py
|
|
7
|
+
src/keytidy/core.py
|
|
8
|
+
src/keytidy.egg-info/PKG-INFO
|
|
9
|
+
src/keytidy.egg-info/SOURCES.txt
|
|
10
|
+
src/keytidy.egg-info/dependency_links.txt
|
|
11
|
+
src/keytidy.egg-info/entry_points.txt
|
|
12
|
+
src/keytidy.egg-info/top_level.txt
|
|
13
|
+
tests/test_core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
keytidy
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from keytidy import core
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def keys_of(s):
|
|
9
|
+
return list(json.loads(s).keys())
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---- order_package_keys ----------------------------------------------------
|
|
13
|
+
|
|
14
|
+
def test_order_package_keys():
|
|
15
|
+
assert core.order_package_keys(["dependencies", "name", "zzz", "version", "aaa"]) == \
|
|
16
|
+
["name", "version", "dependencies", "aaa", "zzz"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---- detect_indent ---------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
def test_detect_indent():
|
|
22
|
+
assert core.detect_indent('{\n "a": 1\n}') == 2
|
|
23
|
+
assert core.detect_indent('{\n "a": 1\n}') == 4
|
|
24
|
+
assert core.detect_indent('{\n\t"a": 1\n}') == "\t"
|
|
25
|
+
assert core.detect_indent("{}") == 2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---- generic JSON ----------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def test_generic_sort_recursive():
|
|
31
|
+
src = json.dumps({"b": 1, "a": 2, "nested": {"z": 1, "y": 2}}, indent=2)
|
|
32
|
+
out = core.process(src, {"packageJson": False})["output"]
|
|
33
|
+
assert keys_of(out) == ["a", "b", "nested"]
|
|
34
|
+
assert list(json.loads(out)["nested"].keys()) == ["y", "z"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_arrays_keep_order_but_objects_inside_sort():
|
|
38
|
+
src = json.dumps({"list": [3, 1, 2], "rows": [{"b": 1, "a": 2}]})
|
|
39
|
+
parsed = json.loads(core.process(src, {})["output"])
|
|
40
|
+
assert parsed["list"] == [3, 1, 2]
|
|
41
|
+
assert list(parsed["rows"][0].keys()) == ["a", "b"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---- package.json ----------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
PKG = json.dumps({
|
|
47
|
+
"version": "1.0.0",
|
|
48
|
+
"scripts": {"test": "t", "build": "b"},
|
|
49
|
+
"name": "x",
|
|
50
|
+
"dependencies": {"b": "1", "a": "1"},
|
|
51
|
+
"description": "d",
|
|
52
|
+
"customField": 1,
|
|
53
|
+
}, indent=2)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_package_top_level_order():
|
|
57
|
+
out = core.process(PKG, {"packageJson": True})["output"]
|
|
58
|
+
assert keys_of(out) == ["name", "version", "description", "scripts", "dependencies", "customField"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_deps_sorted_scripts_preserved():
|
|
62
|
+
parsed = json.loads(core.process(PKG, {"packageJson": True})["output"])
|
|
63
|
+
assert list(parsed["dependencies"].keys()) == ["a", "b"]
|
|
64
|
+
assert list(parsed["scripts"].keys()) == ["test", "build"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_sort_scripts_flag():
|
|
68
|
+
parsed = json.loads(core.process(PKG, {"packageJson": True, "sortScripts": True})["output"])
|
|
69
|
+
assert list(parsed["scripts"].keys()) == ["build", "test"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_all_keys_flag():
|
|
73
|
+
out = core.process(PKG, {"packageJson": True, "allKeys": True})["output"]
|
|
74
|
+
assert keys_of(out) == ["customField", "dependencies", "description", "name", "scripts", "version"]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_non_package_file_is_plain_alphabetical():
|
|
78
|
+
out = core.process(PKG, {"packageJson": False})["output"]
|
|
79
|
+
assert keys_of(out) == ["customField", "dependencies", "description", "name", "scripts", "version"]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---- idempotency & formatting ---------------------------------------------
|
|
83
|
+
|
|
84
|
+
def test_idempotent():
|
|
85
|
+
once = core.process(PKG, {"packageJson": True})["output"]
|
|
86
|
+
twice = core.process(once, {"packageJson": True})
|
|
87
|
+
assert twice["changed"] is False
|
|
88
|
+
assert twice["output"] == once
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_trailing_newline_preserved():
|
|
92
|
+
assert core.process('{"b":1,"a":2}\n', {})["output"].endswith("\n")
|
|
93
|
+
assert not core.process('{"b":1,"a":2}', {})["output"].endswith("\n")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_indent_override():
|
|
97
|
+
out = core.process('{\n "b": 1,\n "a": 2\n}', {"indent": 4})["output"]
|
|
98
|
+
assert '\n "a"' in out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_invalid_json_raises():
|
|
102
|
+
with pytest.raises(ValueError):
|
|
103
|
+
core.process("{not json}", {})
|