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 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
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """keytidy — tidy JSON key order, with smart package.json handling. Zero dependencies."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -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,2 @@
1
+ [console_scripts]
2
+ keytidy = keytidy.cli:main
@@ -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}", {})