Skip to content

Commit

Permalink
feat: adding a schema command
Browse files Browse the repository at this point in the history
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
  • Loading branch information
henryiii committed Nov 15, 2024
1 parent 343fe92 commit 075b7ff
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 1 deletion.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,6 @@ overrides = [
"virtualenv.*",
], ignore_missing_imports = true },
]

[tool.uv]
environments = [ "python_version >= '3.10'" ]
8 changes: 7 additions & 1 deletion src/tox/config/sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys
from abc import ABC, abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Iterator, Mapping, Sequence, TypeVar, cast
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, Mapping, Sequence, TypeVar, cast

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

def get_configs(self) -> Generator[ConfigDefinition[Any], None, None]:
""":return: a mapping of config keys to their definitions"""
for k, v in self._defined.items():
if k == next(iter(v.keys)):
yield v

@abstractmethod
def register_config(self) -> None:
raise NotImplementedError
Expand Down
2 changes: 2 additions & 0 deletions src/tox/plugin/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def _register_plugins(self, inline: ModuleType | None) -> None:
legacy,
list_env,
quickstart,
schema,
show_config,
version_flag,
)
Expand All @@ -60,6 +61,7 @@ def _register_plugins(self, inline: ModuleType | None) -> None:
exec_,
quickstart,
show_config,
schema,
devenv,
list_env,
depends,
Expand Down
114 changes: 114 additions & 0 deletions src/tox/session/cmd/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Generate schema for tox configuration, respecting the current plugins."""

from __future__ import annotations

import json
import sys
import typing
from pathlib import Path
from typing import TYPE_CHECKING

import packaging.requirements
import packaging.version

import tox.config.set_env
import tox.config.types
import tox.tox_env.python.pip.req_file
from tox.plugin import impl

if TYPE_CHECKING:
from tox.config.cli.parser import ToxParser
from tox.config.sets import ConfigSet
from tox.session.state import State


@impl
def tox_add_option(parser: ToxParser) -> None:
parser.add_command("schema", [], "Generate schema for tox configuration", gen_schema)


def _process_type(of_type: typing.Any) -> dict[str, typing.Any]: # noqa: PLR0911
if of_type in {
Path,
str,
packaging.version.Version,
packaging.requirements.Requirement,
tox.tox_env.python.pip.req_file.PythonDeps,
}:
return {"type": "string"}
if of_type is bool:
return {"type": "boolean"}
if of_type is float:
return {"type": "number"}
if of_type is tox.config.types.Command:
return {"type": "array", "items": {"#ref": "#/definitions/command"}}
if of_type is tox.config.types.EnvList:
return {"type": "array", "items": {"type": "string"}}
if typing.get_origin(of_type) in {list, set}:
return {"type": "array", "items": _process_type(typing.get_args(of_type)[0])}
if of_type is tox.config.set_env.SetEnv:
return {
"type": "object",
"additionalProperties": {"type": "array", "items": {"type": "string"}},
}
if typing.get_origin(of_type) is dict:
return {
"type": "object",
"additionalProperties": {"type": "array", "items": _process_type(typing.get_args(of_type)[1])},
}
msg = f"Unknown type: {of_type}"
raise ValueError(msg)


def _get_schema(conf: ConfigSet, path: str) -> dict[str, dict[str, typing.Any]]:
properties = {}
for x in conf.get_configs():
name, *aliases = x.keys
of_type = getattr(x, "of_type", None)
if of_type is None:
continue
desc = getattr(x, "desc", None)
try:
properties[name] = {**_process_type(of_type), "description": desc}
except ValueError:
print(name, "has unrecoginsed type:", of_type, file=sys.stderr) # noqa: T201
for alias in aliases:
properties[alias] = {"$ref": f"{path}/{name}"}
return properties


def gen_schema(state: State) -> int:
core = state.conf.core

properties = _get_schema(core, path="#/properties")

env_properties = _get_schema(state.envs["3.13"].conf, path="#/properties/env_run_base/properties")

json_schema = {
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://github.com/tox-dev/tox/blob/main/src/tox/util/tox.schema.json",
"type": "object",
"properties": {
**properties,
"env_run_base": {"type": "object", "properties": env_properties},
"env_pkg_base": {"$ref": "#/properties/env_run_base"},
"env": {"type": "object", "patternProperties": {"^.*$": {"$ref": "#/properties/env_run_base"}}},
},
"#definitions": {
"command": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"replace": {"type": "string"},
"default": {"type": "array", "items": {"type": "string"}},
"extend": {"type": "boolean"},
},
},
],
}
},
}
print(json.dumps(json_schema, indent=2)) # noqa: T201
return 0

0 comments on commit 075b7ff

Please sign in to comment.