Skip to content

Commit f8a1667

Browse files
committed
feat: adding a schema command
Signed-off-by: Henry Schreiner <[email protected]>
1 parent 343fe92 commit f8a1667

File tree

4 files changed

+124
-1
lines changed

4 files changed

+124
-1
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ build.targets.sdist.include = [
142142
]
143143
version.source = "vcs"
144144

145+
[tool.uv]
146+
environments = ["python_version >= '3.10'"]
147+
145148
[tool.ruff]
146149
target-version = "py38"
147150
line-length = 120

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: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""Generate schema for tox configuration, respecting the current plugins."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import typing
7+
from pathlib import Path
8+
from typing import TYPE_CHECKING
9+
10+
import packaging.requirements
11+
import packaging.version
12+
13+
import tox.config.set_env
14+
import tox.config.types
15+
from tox.config.sets import ConfigSet
16+
from tox.plugin import impl
17+
import tox.tox_env.python.pip.req_file
18+
19+
if TYPE_CHECKING:
20+
from tox.config.cli.parser import ToxParser
21+
from tox.session.state import State
22+
23+
24+
@impl
25+
def tox_add_option(parser: ToxParser) -> None:
26+
parser.add_command("schema", [], "Generate schema for tox configuration", gen_schema)
27+
28+
29+
def _process_type(of_type: typing.Any) -> dict[str, typing.Any]:
30+
if of_type in {
31+
Path,
32+
str,
33+
packaging.version.Version,
34+
packaging.requirements.Requirement,
35+
tox.tox_env.python.pip.req_file.PythonDeps,
36+
}:
37+
return {"type": "string"}
38+
if of_type is bool:
39+
return {"type": "boolean"}
40+
if of_type is float:
41+
return {"type": "number"}
42+
if of_type is tox.config.types.Command:
43+
return {"type": "array", "items": {"#ref": "#/definitions/command"}}
44+
if of_type is tox.config.types.EnvList:
45+
return {"type": "array", "items": {"type": "string"}}
46+
if typing.get_origin(of_type) in {list, set}:
47+
return {"type": "array", "items": _process_type(typing.get_args(of_type)[0])}
48+
if of_type is tox.config.set_env.SetEnv:
49+
return {
50+
"type": "object",
51+
"additionalProperties": {"type": "array", "items": {"type": "string"}},
52+
}
53+
if typing.get_origin(of_type) is dict:
54+
return {
55+
"type": "object",
56+
"additionalProperties": {"type": "array", "items": _process_type(typing.get_args(of_type)[1])},
57+
}
58+
msg = f"Unknown type: {of_type}"
59+
raise ValueError(msg)
60+
61+
62+
def _get_schema(conf: ConfigSet, path: str) -> dict[str, dict[str, typing.Any]]:
63+
properties = {}
64+
for x in conf.get_configs():
65+
name, *aliases = x.keys
66+
of_type = getattr(x, "of_type", None)
67+
if of_type is None:
68+
continue
69+
desc = getattr(x, "desc", None)
70+
try:
71+
properties[name] = {**_process_type(of_type), "description": desc}
72+
except ValueError:
73+
print(name, "is unrecognised:", of_type)
74+
for alias in aliases:
75+
properties[alias] = {"$ref": f"{path}/{name}"}
76+
return properties
77+
78+
79+
def gen_schema(state: State) -> int:
80+
core = state.conf.core
81+
properties = _get_schema(core, path="#/properties")
82+
83+
env_properties = _get_schema(state.envs["3.13"].conf, path="#/properties/env_run_base/properties")
84+
85+
json_schema = {
86+
"$schema": "http://json-schema.org/draft-07/schema",
87+
"$id": "https://github.com/tox-dev/tox/blob/main/src/tox/util/tox.schema.json",
88+
"type": "object",
89+
"properties": {
90+
**properties,
91+
"env_run_base": {"type": "object", "properties": env_properties},
92+
"env_pkg_base": {"$ref": "#/properties/env_run_base"},
93+
"env": {"type": "object", "patternProperties": {"^.*$": {"$ref": "#/properties/env_run_base"}}},
94+
},
95+
"#definitions": {
96+
"command": {
97+
"oneOf": [
98+
{"type": "string"},
99+
{
100+
"type": "object",
101+
"properties": {
102+
"replace": {"type": "string"},
103+
"default": {"type": "array", "items": {"type": "string"}},
104+
"extend": {"type": "boolean"},
105+
},
106+
},
107+
],
108+
}
109+
},
110+
}
111+
print(json.dumps(json_schema, indent=2))
112+
return 0

0 commit comments

Comments
 (0)