Skip to content

Commit c541a10

Browse files
committed
chore: add a utility to generate a schema file
Signed-off-by: Henry Schreiner <[email protected]>
1 parent c6cb3c6 commit c541a10

File tree

1 file changed

+108
-0
lines changed

1 file changed

+108
-0
lines changed

misc/make_schema.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from __future__ import annotations
2+
3+
import dataclasses
4+
import json
5+
import re
6+
import textwrap
7+
from collections.abc import Generator, Sequence
8+
from pathlib import Path
9+
from typing import Any
10+
11+
DIR = Path(__file__).parent.resolve()
12+
13+
14+
@dataclasses.dataclass(frozen=True)
15+
class ConfigOpt:
16+
name: str
17+
type_str: str
18+
is_global: bool
19+
description: str
20+
default: str | None
21+
22+
def define(self) -> dict[str, str]:
23+
retval: dict[str, Any] = {"description": self.description}
24+
if self.default is not None:
25+
match self.type_str:
26+
case "boolean":
27+
retval["default"] = {"true": True, "false": False}[self.default.lower()]
28+
case "integer":
29+
retval["default"] = int(self.default)
30+
case "string":
31+
retval["default"] = self.default.strip("`")
32+
case _:
33+
msg = f"Default not suppored for {self.type_str}"
34+
raise RuntimeError(msg)
35+
match self.type_str:
36+
case "boolean" | "integer" | "string":
37+
retval["type"] = self.type_str
38+
case "comma-separated list of strings" | "regular expression":
39+
retval["oneOf"] = [
40+
{"type": "string"},
41+
{"type": "array", "items": {"type": "string"}},
42+
]
43+
case _:
44+
msg = f"{self.type_str} not supported for type"
45+
raise RuntimeError(msg)
46+
47+
return retval
48+
49+
50+
def parse_rst_docs(txt: str) -> Generator[ConfigOpt, None, None]:
51+
for match in re.finditer(r".. confval:: ([^\s]*)\n\n((?: .*\n|\n)*)", txt):
52+
name, body = match.groups()
53+
body = textwrap.dedent(body)
54+
body_match = re.match(r":type: (.*?)\n(?::default: (.*?)\n)?(.*)$", body, re.DOTALL)
55+
assert body_match is not None
56+
type_str, default, inner = body_match.groups()
57+
is_global = "only be set in the global section" in body
58+
description = inner.strip().split("\n\n")[0].replace("\n", " ")
59+
# Patches
60+
if name == "mypy_path":
61+
type_str = "comma-separated list of strings"
62+
yield ConfigOpt(
63+
name=name,
64+
type_str=type_str,
65+
is_global=is_global,
66+
description=description,
67+
default=default,
68+
)
69+
70+
71+
def make_schema(opts: Sequence[ConfigOpt]) -> dict[str, Any]:
72+
definitions = {s.name: s.define() for s in opts}
73+
overrides = {s.name: {"$ref": f"#/properties/{s.name}"} for s in opts if not s.is_global}
74+
module = {
75+
"oneOf": [
76+
{"type": "string"},
77+
{"type": "array", "items": {"type": "string"}, "minItems": 1},
78+
]
79+
}
80+
81+
# Improve some fields
82+
definitions["follow_imports"]["enum"] = ["normal", "silent", "skip", "error"]
83+
84+
return {
85+
"$id": "https://json.schemastore.org/mypy.json",
86+
"$schema": "http://json-schema.org/draft-07/schema#",
87+
"additionalProperties": False,
88+
"type": "object",
89+
"properties": {
90+
**definitions,
91+
"overrides": {
92+
"type": "array",
93+
"items": {
94+
"type": "object",
95+
"additionalProperties": False,
96+
"required": ["module"],
97+
"minProperties": 2,
98+
"properties": {"module": module, **overrides},
99+
},
100+
},
101+
},
102+
}
103+
104+
105+
if __name__ == "__main__":
106+
filepath = DIR.parent / "docs/source/config_file.rst"
107+
opts = parse_rst_docs(filepath.read_text())
108+
print(json.dumps(make_schema(list(opts)), indent=2))

0 commit comments

Comments
 (0)