check-json5 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- check_json5/__init__.py +1 -0
- check_json5/check_json5.py +269 -0
- check_json5-1.3.0.dist-info/METADATA +158 -0
- check_json5-1.3.0.dist-info/RECORD +8 -0
- check_json5-1.3.0.dist-info/WHEEL +5 -0
- check_json5-1.3.0.dist-info/entry_points.txt +2 -0
- check_json5-1.3.0.dist-info/licenses/LICENSE +10 -0
- check_json5-1.3.0.dist-info/top_level.txt +1 -0
check_json5/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pre-commit hook to check JSON files for syntax parseable as JSON5 (with comments)."""
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Based on https://gitlab.com/bmares/check-json5/-/blob/db9b5c0f76dea9b0f28097b4421e4b40f39b2266/pypackages/pre_commit_hooks/check_json5.py."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
# The hook starts once per commit, so its imports are a large share of a run:
|
|
11
|
+
# the type-only ones load for the type checker alone. `re` is free: json imports it.
|
|
12
|
+
TYPE_CHECKING = False
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Callable, Iterable, Iterator, Sequence
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
# (line, column, message); line and column are 1-based, or None when the parser gives no position.
|
|
18
|
+
Problem = tuple[int | None, int | None, str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DuplicateKeyError(ValueError):
|
|
22
|
+
"""A JSON object repeats a key."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, key: str) -> None:
|
|
25
|
+
"""Keep the repeated key for the error message."""
|
|
26
|
+
super().__init__(f"Duplicate key: {key}")
|
|
27
|
+
self.key = key
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NonStandardConstantError(ValueError):
|
|
31
|
+
"""Strict JSON contains NaN, Infinity or -Infinity."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def raise_duplicate_keys(ordered_pairs: Iterable[tuple[str, Any]]) -> dict[str, Any]:
|
|
35
|
+
"""Build a dict from a JSON object's pairs, refusing a repeated key.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
DuplicateKeyError: If a key repeats.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
d = {}
|
|
42
|
+
for key, val in ordered_pairs:
|
|
43
|
+
if key in d:
|
|
44
|
+
raise DuplicateKeyError(key)
|
|
45
|
+
d[key] = val
|
|
46
|
+
return d
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _reject_constant(name: str) -> float:
|
|
50
|
+
raise NonStandardConstantError(name)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Only ever run over text a parser has looked at, so it can be loose: it skips what it
|
|
54
|
+
# does not recognise. Strings come before comments so a "//" inside one stays a string.
|
|
55
|
+
_TOKEN = re.compile(
|
|
56
|
+
r"""(?P<str>"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')
|
|
57
|
+
| (?P<skip>\s+|//[^\n]*|/\*.*?\*/)
|
|
58
|
+
| (?P<punct>[{}\[\]:,])
|
|
59
|
+
| (?P<bare>[^\s{}\[\]:,"'/]+)""",
|
|
60
|
+
re.VERBOSE | re.DOTALL,
|
|
61
|
+
)
|
|
62
|
+
_IDENTIFIER = re.compile(r"[A-Za-z_$][\w$]*")
|
|
63
|
+
_JSON5_ERROR = re.compile(r"<string>:(\d+) (.*) at column (\d+)", re.DOTALL)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _tokens(text: str) -> Iterator[tuple[str, str, int]]:
|
|
67
|
+
for m in _TOKEN.finditer(text):
|
|
68
|
+
if m.lastgroup != "skip":
|
|
69
|
+
yield m.lastgroup or "", m.group(), m.start()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _member(path: str, key: str) -> str:
|
|
73
|
+
return f"{path}.{key}" if _IDENTIFIER.fullmatch(key) else f"{path}[{json.dumps(key)}]"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _duplicates(text: str, decode: Callable[[str], Any]) -> list[tuple[int, str]]:
|
|
77
|
+
"""Offset and JSONPath of every key that repeats an earlier key of the same object."""
|
|
78
|
+
found: list[tuple[int, str]] = []
|
|
79
|
+
# One frame per open container: [bracket, its path, keys seen (objects), current key or index].
|
|
80
|
+
stack: list[list[Any]] = []
|
|
81
|
+
prev = ("", "", 0)
|
|
82
|
+
for kind, tok, offset in _tokens(text):
|
|
83
|
+
top = stack[-1] if stack else None
|
|
84
|
+
if kind != "punct":
|
|
85
|
+
pass
|
|
86
|
+
elif tok in "{[":
|
|
87
|
+
stack.append([tok, _child_path(top), set(), None if tok == "{" else 0])
|
|
88
|
+
elif tok in "}]":
|
|
89
|
+
if stack:
|
|
90
|
+
stack.pop()
|
|
91
|
+
elif top is None:
|
|
92
|
+
pass
|
|
93
|
+
elif tok == "," and top[0] == "[":
|
|
94
|
+
top[3] += 1
|
|
95
|
+
elif tok == ":" and top[0] == "{":
|
|
96
|
+
key = decode(prev[1]) if prev[0] == "str" else prev[1]
|
|
97
|
+
if key in top[2]:
|
|
98
|
+
found.append((prev[2], _member(top[1], key)))
|
|
99
|
+
top[2].add(key)
|
|
100
|
+
top[3] = key
|
|
101
|
+
prev = (kind, tok, offset)
|
|
102
|
+
return found
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _child_path(parent: list[Any] | None) -> str:
|
|
106
|
+
if parent is None:
|
|
107
|
+
return "$"
|
|
108
|
+
if parent[0] == "{":
|
|
109
|
+
return _member(parent[1], parent[3] or "")
|
|
110
|
+
return f"{parent[1]}[{parent[3]}]"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _position(text: str, offset: int) -> tuple[int, int]:
|
|
114
|
+
return text.count("\n", 0, offset) + 1, offset - text.rfind("\n", 0, offset)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _duplicate_problems(text: str, decode: Callable[[str], Any], exc: DuplicateKeyError) -> list[Problem]:
|
|
118
|
+
found = _duplicates(text, decode)
|
|
119
|
+
if not found: # the scanner missed what the parser saw: still fail, just without a place
|
|
120
|
+
return [(None, None, f"duplicate key {exc.key!r}")]
|
|
121
|
+
return [(*_position(text, offset), f"duplicate key {path}") for offset, path in found]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def check(text: str, *, strict: bool = False) -> list[Problem]:
|
|
125
|
+
"""Check one file's text: JSON5 syntax and duplicate keys, or strict JSON when `strict`.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The problems found, empty when the text passes.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
RecursionError: If the text nests deeper than the parser can follow.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
problems = _check_json(text, strict=strict)
|
|
135
|
+
return _check_json5(text) if problems is None else problems
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _check_json(text: str, *, strict: bool) -> list[Problem] | None:
|
|
139
|
+
"""Problems as strict JSON, or None when the text is not strict JSON and may be JSON5."""
|
|
140
|
+
try:
|
|
141
|
+
# Fast path: strict JSON parses at C speed and covers most files.
|
|
142
|
+
if strict:
|
|
143
|
+
json.loads(text, object_pairs_hook=raise_duplicate_keys, parse_constant=_reject_constant)
|
|
144
|
+
else:
|
|
145
|
+
json.loads(text, object_pairs_hook=raise_duplicate_keys)
|
|
146
|
+
except DuplicateKeyError as exc:
|
|
147
|
+
return _duplicate_problems(text, json.loads, exc)
|
|
148
|
+
except NonStandardConstantError:
|
|
149
|
+
return [
|
|
150
|
+
(*_position(text, offset), f"{tok} is not valid in strict JSON")
|
|
151
|
+
for kind, tok, offset in _tokens(text)
|
|
152
|
+
if kind == "bare" and tok in {"NaN", "Infinity", "-Infinity"}
|
|
153
|
+
]
|
|
154
|
+
except json.JSONDecodeError as exc:
|
|
155
|
+
return [(exc.lineno, exc.colno, f"not strict JSON: {exc.msg}")] if strict else None
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _check_json5(text: str) -> list[Problem]:
|
|
160
|
+
# Imported here so a run over strict JSON never pays json5's import time.
|
|
161
|
+
# NOTE: a top-level `lazy import` does the same, once 3.15 is the floor.
|
|
162
|
+
import json5 # noqa: PLC0415
|
|
163
|
+
|
|
164
|
+
# json5 re-raises an error from the hook as a plain ValueError, so keep the original here.
|
|
165
|
+
duplicate: list[DuplicateKeyError] = []
|
|
166
|
+
|
|
167
|
+
def hook(pairs: Iterable[tuple[str, Any]]) -> dict[str, Any]:
|
|
168
|
+
try:
|
|
169
|
+
return raise_duplicate_keys(pairs)
|
|
170
|
+
except DuplicateKeyError as exc:
|
|
171
|
+
duplicate.append(exc)
|
|
172
|
+
raise
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
json5.loads(text, object_pairs_hook=hook)
|
|
176
|
+
except ValueError as exc:
|
|
177
|
+
if duplicate:
|
|
178
|
+
return _duplicate_problems(text, json5.loads, duplicate[0])
|
|
179
|
+
m = _JSON5_ERROR.fullmatch(str(exc))
|
|
180
|
+
return [(int(m[1]), int(m[3]), m[2])] if m else [(None, None, str(exc))]
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def is_strict(filename: str, patterns: Sequence[str]) -> bool:
|
|
185
|
+
"""Whether `filename` must be strict JSON: the last matching pattern decides.
|
|
186
|
+
|
|
187
|
+
A pattern without a `/` matches the file name at any depth, one with a `/` the whole
|
|
188
|
+
path, `*` matches across `/`, and a leading `!` exempts the files it matches.
|
|
189
|
+
"""
|
|
190
|
+
if not patterns:
|
|
191
|
+
return False
|
|
192
|
+
from fnmatch import fnmatchcase # noqa: PLC0415
|
|
193
|
+
|
|
194
|
+
path = filename.replace(os.sep, "/")
|
|
195
|
+
name = path.rsplit("/", 1)[-1]
|
|
196
|
+
strict = False
|
|
197
|
+
for pattern in patterns:
|
|
198
|
+
bare = pattern.removeprefix("!")
|
|
199
|
+
if fnmatchcase(path if "/" in bare else name, bare):
|
|
200
|
+
strict = not pattern.startswith("!")
|
|
201
|
+
return strict
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _escape(value: str, *, prop: bool = False) -> str:
|
|
205
|
+
value = value.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
|
|
206
|
+
return value.replace(":", "%3A").replace(",", "%2C") if prop else value
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def report(filename: str, problem: Problem, *, annotate: bool = False) -> None:
|
|
210
|
+
"""Print one problem as `file:line:col: message`, plus a GitHub annotation when `annotate`."""
|
|
211
|
+
line, col, msg = problem
|
|
212
|
+
print(f"{filename}: {msg}" if line is None else f"{filename}:{line}:{col}: {msg}")
|
|
213
|
+
if annotate:
|
|
214
|
+
where = "" if line is None else f",line={line},col={col}"
|
|
215
|
+
print(f"::error file={_escape(filename, prop=True)}{where},title=check-json5::{_escape(msg)}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
219
|
+
"""Check JSON5 files for syntax errors and duplicate keys.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
argv: options and filenames. If None, defaults to sys.argv[1:].
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
An exit code indicating success (0) or failure (1).
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
args = sys.argv[1:] if argv is None else list(argv)
|
|
229
|
+
strict_patterns: list[str] = []
|
|
230
|
+
if any(a.startswith("-") for a in args): # argparse costs a few ms, so only load it for options
|
|
231
|
+
import argparse # noqa: PLC0415
|
|
232
|
+
|
|
233
|
+
parser = argparse.ArgumentParser(prog="check-json5", description="Check JSON5 files.")
|
|
234
|
+
parser.add_argument(
|
|
235
|
+
"--strict",
|
|
236
|
+
action="append",
|
|
237
|
+
default=[],
|
|
238
|
+
metavar="GLOB",
|
|
239
|
+
help="files matching GLOB must be strict JSON; repeat it, `!GLOB` exempts (last match wins)",
|
|
240
|
+
)
|
|
241
|
+
parser.add_argument("filenames", nargs="*")
|
|
242
|
+
ns = parser.parse_args(args)
|
|
243
|
+
args, strict_patterns = ns.filenames, ns.strict
|
|
244
|
+
|
|
245
|
+
# Python caps int literals at 4300 digits (3.10.7+), which fails valid JSON with a long number.
|
|
246
|
+
if hasattr(sys, "set_int_max_str_digits"):
|
|
247
|
+
sys.set_int_max_str_digits(0)
|
|
248
|
+
annotate = os.environ.get("GITHUB_ACTIONS") == "true"
|
|
249
|
+
retval = 0
|
|
250
|
+
for filename in args:
|
|
251
|
+
if not os.path.isfile(filename): # noqa: PTH113
|
|
252
|
+
problems: list[Problem] = [(None, None, "No such file")]
|
|
253
|
+
else:
|
|
254
|
+
try:
|
|
255
|
+
# utf-8-sig transparently strips a leading BOM if present.
|
|
256
|
+
with open(filename, encoding="utf-8-sig") as f: # noqa: PTH123
|
|
257
|
+
problems = check(f.read(), strict=is_strict(filename, strict_patterns))
|
|
258
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
259
|
+
problems = [(None, None, f"Failed to read ({exc})")]
|
|
260
|
+
except RecursionError: # fails this file instead of ending the run
|
|
261
|
+
problems = [(None, None, "nested too deeply to check")]
|
|
262
|
+
for problem in problems:
|
|
263
|
+
report(filename, problem, annotate=annotate)
|
|
264
|
+
retval |= bool(problems)
|
|
265
|
+
return retval
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: check-json5
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: pre-commit hook to check json files for syntax parseable as json5 (with comments)
|
|
5
|
+
Author-email: Simon van Lierde <30519500-simonvanlierde@users.noreply.gitlab.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Changelog, https://github.com/simonvanlierde/check-json5/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/simonvanlierde/check-json5/issues
|
|
9
|
+
Project-URL: Source, https://github.com/simonvanlierde/check-json5
|
|
10
|
+
Keywords: json,json5,jsonc,lint,pre-commit
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: json5>=0.15.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# check-json5
|
|
21
|
+
|
|
22
|
+
[](https://github.com/simonvanlierde/check-json5/actions/workflows/ci.yml)
|
|
23
|
+
[](https://codecov.io/gh/simonvanlierde/check-json5)
|
|
24
|
+
[](https://www.python.org/downloads/)
|
|
25
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
26
|
+
[](LICENSE)
|
|
27
|
+
|
|
28
|
+
A [pre-commit](https://pre-commit.com/) hook that checks whether `.json` files parse as
|
|
29
|
+
[JSON5](https://json5.org/) (JSON with comments, trailing commas, and more). It replaces the
|
|
30
|
+
standard `check-json` hook, accepts JSON5 too, and keeps the speed of the C parser on ordinary
|
|
31
|
+
JSON.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Accepts JSON5 syntax (comments, trailing commas) that the standard `check-json` hook rejects.
|
|
36
|
+
- Every failure names its place, as `file:line:col: message`, so editors and terminals link
|
|
37
|
+
straight to it. A duplicate key also gets its path, and every repeat in a file is listed:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
config.json:4:5: duplicate key $.server.port
|
|
41
|
+
config.json:9:3: duplicate key $.plugins[2].name
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- On GitHub Actions, each failure is also an annotation on the line of the pull request diff.
|
|
45
|
+
- `--strict GLOB` holds chosen files to strict JSON (no comments, no `NaN`), for files that a
|
|
46
|
+
JSON parser reads at runtime, such as `package.json`.
|
|
47
|
+
- Fast path: strict JSON is parsed with the C-speed stdlib `json` parser, falling back to the
|
|
48
|
+
pure-Python `json5` parser only when a file actually uses JSON5 features (~500× faster on large
|
|
49
|
+
strict-JSON files; see [Performance](#performance)).
|
|
50
|
+
- Accepts `NaN` and `Infinity` (both parsers allow them) unless `--strict` covers the file.
|
|
51
|
+
|
|
52
|
+
## Links
|
|
53
|
+
|
|
54
|
+
- 🐙 [GitHub (main)](https://github.com/simonvanlierde/check-json5)
|
|
55
|
+
- 🦊 [GitLab (mirror)](https://gitlab.com/simonvanlierde/check-json5)
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
In `.pre-commit-config.yaml`, under `repos:`, add:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
- repo: https://github.com/simonvanlierde/check-json5
|
|
63
|
+
rev: v1.3.0
|
|
64
|
+
hooks:
|
|
65
|
+
- id: check-json5
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Remove the original `check-json` hook if it is already included. A file passes when it parses as
|
|
69
|
+
JSON5 and has no duplicate keys. The hook picks files by extension (`.json`, `.jsonc`, `.json5`,
|
|
70
|
+
`.geojson`, `.jsonld`), while `check-json` picks them by type, so files such as `.babelrc` or
|
|
71
|
+
`.ipynb` are no longer checked after the swap. Add them with `files:` if you need them.
|
|
72
|
+
|
|
73
|
+
### Strict files
|
|
74
|
+
|
|
75
|
+
Some JSON files are read by a strict parser at runtime: a comment in `package.json` passes a JSON5
|
|
76
|
+
check and then breaks `npm`. Name those files with `--strict`. Each pattern matches the file name
|
|
77
|
+
at any depth, or the whole path when it contains a `/`. `*` also matches across `/`. A leading `!`
|
|
78
|
+
exempts files, and the last matching pattern wins:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
- id: check-json5
|
|
82
|
+
args: [--strict, "*.json", --strict, "!tsconfig*.json", --strict, "!.vscode/*"]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Without pre-commit
|
|
86
|
+
|
|
87
|
+
The hook is also a command on PyPI:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uvx check-json5 --strict package.json config/*.json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Introduction
|
|
94
|
+
|
|
95
|
+
JSON5 is similar to JSON but permits comments, trailing commas, and more. It resembles "JSONC" (JSON
|
|
96
|
+
with Comments) but, unlike JSONC, has an actual specification. Many config files in the wild are
|
|
97
|
+
really JSON5, so the standard `check-json` hook, which rejects comments outright, flags them as
|
|
98
|
+
invalid. This hook accepts them.
|
|
99
|
+
|
|
100
|
+
Strict-JSON files are parsed with the fast built-in [`json`](https://docs.python.org/3/library/json.html)
|
|
101
|
+
library; only files that use JSON5 features fall back to the pure-Python
|
|
102
|
+
[`json5`](https://pypi.org/project/json5/) library.
|
|
103
|
+
|
|
104
|
+
## Performance
|
|
105
|
+
|
|
106
|
+
The pure-Python `json5` parser is slow on large files. Since most `.json` files are strict JSON, the
|
|
107
|
+
hook tries the C-speed stdlib `json` parser first and only falls back to `json5` when a file uses
|
|
108
|
+
comments or trailing commas. Duplicate-key detection works on both paths.
|
|
109
|
+
|
|
110
|
+
Time for one hook call per file (strict JSON, taking the fast path), best of 10 runs of
|
|
111
|
+
`uv run python scripts/bench.py` on 2026-10-11 (Intel i9-14900KF, Linux, Python 3.13.7, json5 0.15.0,
|
|
112
|
+
load average 1.5):
|
|
113
|
+
|
|
114
|
+
| File size | Before (`json5` only) | After (fast path) | Speedup |
|
|
115
|
+
| --------- | --------------------- | ----------------- | ------- |
|
|
116
|
+
| 1 KB | 2.5 ms | 0.02 ms | ~100× |
|
|
117
|
+
| 47 KB | 123 ms | 0.19 ms | ~660× |
|
|
118
|
+
| 483 KB | 1.3 s | 1.8 ms | ~750× |
|
|
119
|
+
| 2 MB | 5.4 s | 7.7 ms | ~700× |
|
|
120
|
+
|
|
121
|
+
The script generates its fixture files in a temporary directory, so nothing large is committed.
|
|
122
|
+
Timings vary between runs by up to 2× on a loaded machine. The 1 KB fast-path time is close to
|
|
123
|
+
the timer's resolution, so its speed-up ranged from ~80× to ~100× across runs.
|
|
124
|
+
|
|
125
|
+
To time whole hook runs instead, run `bash scripts/bench.sh` (needs
|
|
126
|
+
[hyperfine](https://github.com/sharkdp/hyperfine)). It times the hook against the `json5` parser
|
|
127
|
+
on 50 KB, 500 KB and 2 MB files and prints one table; `--quick` times the 50 KB file once. Each
|
|
128
|
+
run includes about 20 ms of Python startup, so its speed-ups are smaller than the ones above.
|
|
129
|
+
|
|
130
|
+
Files that use JSON5 syntax still go through the `json5` parser and are unchanged. For tiny config
|
|
131
|
+
files the absolute saving is negligible next to Python's own startup cost; the win matters on large
|
|
132
|
+
JSON files.
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
The project uses [uv](https://github.com/astral-sh/uv) for dependency management, `pytest` for
|
|
137
|
+
tests, `pre-commit` for local checks, and GitHub Actions for CI with coverage reported to Codecov.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
uv sync # install dependencies
|
|
141
|
+
uv run pytest # run the test suite
|
|
142
|
+
uv run pytest --cov # tests with coverage
|
|
143
|
+
uv run pre-commit run -a # run the configured hooks locally
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Credits
|
|
147
|
+
|
|
148
|
+
Adapted by Simon van Lierde from the original [check-json5](https://gitlab.com/bmares/check-json5) by
|
|
149
|
+
Ben Mares, itself based on the
|
|
150
|
+
[pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks) repository by Anthony Sottile and
|
|
151
|
+
contributors. This version switches dependency management from Poetry to uv and adds the fast-path
|
|
152
|
+
parser and benchmark above.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
[MIT](LICENSE), consistent with the upstream projects it is based on
|
|
157
|
+
([Ben Mares](https://gitlab.com/bmares/check-json5/-/blob/main/LICENSE),
|
|
158
|
+
[pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks/blob/main/LICENSE)).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
check_json5/__init__.py,sha256=8zSYfR0TaxiTShizu9YH8TJ4P4ddS3qM-3XfC5wO380,89
|
|
2
|
+
check_json5/check_json5.py,sha256=OH11toCvP09JabJwBQcTTPV8uBrfR0UPi39t_u6EXC0,10082
|
|
3
|
+
check_json5-1.3.0.dist-info/licenses/LICENSE,sha256=tVGACWS_DERmpA4nTgZ41iHhqE2-x7hmTp_rzcNfkHU,1110
|
|
4
|
+
check_json5-1.3.0.dist-info/METADATA,sha256=LUlIYexzrkZDBxdY5P5URUAJaxlNqhntJbMwU2CqwCg,7432
|
|
5
|
+
check_json5-1.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
check_json5-1.3.0.dist-info/entry_points.txt,sha256=-jKwoj1vJv13w_U9G1oHSzVpnkjLz6OiACRaMR0l11k,61
|
|
7
|
+
check_json5-1.3.0.dist-info/top_level.txt,sha256=5_Pwwyf8-UpH80eRmR62t2odl8GW5PjB9c-qo80Nhuw,12
|
|
8
|
+
check_json5-1.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Simon van Lierde
|
|
4
|
+
Copyright (c) 2021 Ben Mares
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
check_json5
|