Skip to content

Commit b2c3c50

Browse files
committed
feat: adding a schema command
Now running this passes: uvx check-jsonschema --schemafile src/tox/tox.schema.json tox.toml Signed-off-by: Henry Schreiner <[email protected]>
1 parent dbbb043 commit b2c3c50

File tree

6 files changed

+628
-1
lines changed

6 files changed

+628
-1
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,6 @@ overrides = [
253253
"virtualenv.*",
254254
], ignore_missing_imports = true },
255255
]
256+
257+
[tool.uv]
258+
environments = [ "python_version >= '3.10'" ]

src/tox/config/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ def core(self) -> CoreConfigSet:
123123
self._core_set = core
124124
return core
125125

126+
def all_section_configs(self) -> Iterator[tuple[tuple[str, str, str], ConfigSet]]:
127+
yield from self._key_to_conf_set.items()
128+
126129
def get_section_config(
127130
self,
128131
section: Section,

src/tox/config/sets.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44
from abc import ABC, abstractmethod
55
from pathlib import Path
6-
from typing import TYPE_CHECKING, Any, Callable, Iterator, Mapping, Sequence, TypeVar, cast
6+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, Mapping, Sequence, TypeVar, cast
77

88
from .of_type import ConfigConstantDefinition, ConfigDefinition, ConfigDynamicDefinition, ConfigLoadArgs
99
from .set_env import SetEnv
@@ -33,6 +33,12 @@ def __init__(self, conf: Config, section: Section, env_name: str | None) -> None
3333
self._final = False
3434
self.register_config()
3535

36+
def get_configs(self) -> Generator[ConfigDefinition[Any], None, None]:
37+
""":return: a mapping of config keys to their definitions"""
38+
for k, v in self._defined.items():
39+
if k == next(iter(v.keys)):
40+
yield v
41+
3642
@abstractmethod
3743
def register_config(self) -> None:
3844
raise NotImplementedError

src/tox/plugin/manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def _register_plugins(self, inline: ModuleType | None) -> None:
4242
legacy,
4343
list_env,
4444
quickstart,
45+
schema,
4546
show_config,
4647
version_flag,
4748
)
@@ -60,6 +61,7 @@ def _register_plugins(self, inline: ModuleType | None) -> None:
6061
exec_,
6162
quickstart,
6263
show_config,
64+
schema,
6365
devenv,
6466
list_env,
6567
depends,

src/tox/session/cmd/schema.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""Generate schema for tox configuration, respecting the current plugins."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import sys
7+
import typing
8+
from pathlib import Path
9+
from types import NoneType
10+
from typing import TYPE_CHECKING
11+
12+
import packaging.requirements
13+
import packaging.version
14+
15+
import tox.config.set_env
16+
import tox.config.types
17+
import tox.tox_env.python.pip.req_file
18+
from tox.plugin import impl
19+
20+
if TYPE_CHECKING:
21+
from tox.config.cli.parser import ToxParser
22+
from tox.config.sets import ConfigSet
23+
from tox.session.state import State
24+
25+
26+
@impl
27+
def tox_add_option(parser: ToxParser) -> None:
28+
our = parser.add_command("schema", [], "Generate schema for tox configuration", gen_schema)
29+
our.add_argument("--strict", action="store_true", help="Disallow extra properties in configuration")
30+
31+
32+
def _process_type(of_type: typing.Any) -> dict[str, typing.Any]: # noqa: C901, PLR0911
33+
if of_type in {
34+
Path,
35+
str,
36+
packaging.version.Version,
37+
packaging.requirements.Requirement,
38+
tox.tox_env.python.pip.req_file.PythonDeps,
39+
}:
40+
return {"type": "string"}
41+
if typing.get_origin(of_type) is typing.Union:
42+
types = [x for x in typing.get_args(of_type) if x is not NoneType]
43+
if len(types) == 1:
44+
return _process_type(types[0])
45+
msg = f"Union types are not supported: {of_type}"
46+
raise ValueError(msg)
47+
if of_type is bool:
48+
return {"type": "boolean"}
49+
if of_type is float:
50+
return {"type": "number"}
51+
if typing.get_origin(of_type) is typing.Literal:
52+
return {"enum": list(typing.get_args(of_type))}
53+
if of_type in {tox.config.types.Command, tox.config.types.EnvList}:
54+
return {"type": "array", "items": {"$ref": "#/definitions/subs"}}
55+
if typing.get_origin(of_type) in {list, set}:
56+
if typing.get_args(of_type)[0] in {str, packaging.requirements.Requirement}:
57+
return {"type": "array", "items": {"$ref": "#/definitions/subs"}}
58+
if typing.get_args(of_type)[0] is tox.config.types.Command:
59+
return {"type": "array", "items": _process_type(typing.get_args(of_type)[0])}
60+
msg = f"Unknown list type: {of_type}"
61+
raise ValueError(msg)
62+
if of_type is tox.config.set_env.SetEnv:
63+
return {
64+
"type": "object",
65+
"additionalProperties": {"$ref": "#/definitions/subs"},
66+
}
67+
if typing.get_origin(of_type) is dict:
68+
return {
69+
"type": "object",
70+
"additionalProperties": {**_process_type(typing.get_args(of_type)[1])},
71+
}
72+
msg = f"Unknown type: {of_type}"
73+
raise ValueError(msg)
74+
75+
76+
def _get_schema(conf: ConfigSet, path: str) -> dict[str, dict[str, typing.Any]]:
77+
properties = {}
78+
for x in conf.get_configs():
79+
name, *aliases = x.keys
80+
of_type = getattr(x, "of_type", None)
81+
if of_type is None:
82+
continue
83+
desc = getattr(x, "desc", None)
84+
try:
85+
properties[name] = {**_process_type(of_type), "description": desc}
86+
except ValueError:
87+
print(name, "has unrecoginsed type:", of_type, file=sys.stderr) # noqa: T201
88+
for alias in aliases:
89+
properties[alias] = {"$ref": f"{path}/{name}"}
90+
return properties
91+
92+
93+
def gen_schema(state: State) -> int:
94+
core = state.conf.core
95+
strict = state.conf.options.strict
96+
97+
# Accessing this adds extra stuff to core, so we need to do it first
98+
env_properties = _get_schema(state.envs["3.13"].conf, path="#/properties/env_run_base/properties")
99+
100+
properties = _get_schema(core, path="#/properties")
101+
102+
sections = {
103+
key: conf
104+
for s, conf in state.conf.all_section_configs()
105+
if (key := s[0].split(".")[0]) not in {"env_run_base", "env_pkg_base", "env"}
106+
}
107+
for key, conf in sections.items():
108+
properties[key] = {
109+
"type": "object",
110+
"additionalProperties": not strict,
111+
"properties": _get_schema(conf, path=f"#/properties/{key}/properties"),
112+
}
113+
114+
json_schema = {
115+
"$schema": "http://json-schema.org/draft-07/schema",
116+
"$id": "https://github.com/tox-dev/tox/blob/main/src/tox/util/tox.schema.json",
117+
"type": "object",
118+
"properties": {
119+
**properties,
120+
"env_run_base": {
121+
"type": "object",
122+
"properties": env_properties,
123+
"additionalProperties": not strict,
124+
},
125+
"env_pkg_base": {
126+
"$ref": "#/properties/env_run_base",
127+
"additionalProperties": not strict,
128+
},
129+
"env": {"type": "object", "patternProperties": {"^.*$": {"$ref": "#/properties/env_run_base"}}},
130+
"legacy_tox_ini": {"type": "string"},
131+
},
132+
"additionalProperties": not strict,
133+
"definitions": {
134+
"subs": {
135+
"anyOf": [
136+
{"type": "string"},
137+
{
138+
"type": "object",
139+
"properties": {
140+
"replace": {"type": "string"},
141+
"name": {"type": "string"},
142+
"default": {
143+
"oneOf": [
144+
{"type": "string"},
145+
{"type": "array", "items": {"$ref": "#/definitions/subs"}},
146+
]
147+
},
148+
"extend": {"type": "boolean"},
149+
},
150+
"required": ["replace"],
151+
"additionalProperties": False,
152+
},
153+
{
154+
"type": "object",
155+
"properties": {
156+
"replace": {"type": "string"},
157+
"of": {"type": "array", "items": {"type": "string"}},
158+
"default": {
159+
"oneOf": [
160+
{"type": "string"},
161+
{"type": "array", "items": {"$ref": "#/definitions/subs"}},
162+
]
163+
},
164+
"extend": {"type": "boolean"},
165+
},
166+
"required": ["replace", "of"],
167+
"additionalProperties": False,
168+
},
169+
],
170+
},
171+
},
172+
}
173+
print(json.dumps(json_schema, indent=2)) # noqa: T201
174+
return 0

0 commit comments

Comments
 (0)