Skip to content

Commit 075b7ff

Browse files
committed
feat: adding a schema command
Signed-off-by: Henry Schreiner <[email protected]> [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci
1 parent 343fe92 commit 075b7ff

File tree

4 files changed

+126
-1
lines changed

4 files changed

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

0 commit comments

Comments
 (0)