Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/update-schemastore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Update SchemaStore
on:
push:
tags: ["*"]

permissions:
contents: read

jobs:
update-schemastore:
runs-on: ubuntu-24.04
env:
GH_TOKEN: ${{ secrets.SCHEMASTORE_TOKEN }}
BRANCH: update-tox-schema
steps:
- uses: actions/checkout@v6
- name: Fork and clone SchemaStore
run: gh repo fork SchemaStore/schemastore --clone --remote -- /tmp/schemastore
- name: Create or reset branch
run: git switch -C "$BRANCH"
working-directory: /tmp/schemastore
- name: Copy schema with SchemaStore $id
run: |
python3 -c "
import json
with open('${{ github.workspace }}/src/tox/tox.schema.json') as f:
schema = json.load(f)
schema['\$id'] = 'https://json.schemastore.org/tox.json'
with open('/tmp/schemastore/src/schemas/json/tox.json', 'w') as f:
json.dump(schema, f, indent=2)
f.write('\n')
"
- name: Check for changes
id: diff
run: |
git add src/schemas/json/tox.json
if git diff --cached --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
working-directory: /tmp/schemastore
- name: Commit and push
if: steps.diff.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "Update tox JSON Schema to ${{ github.ref_name }}"
git push --force origin "$BRANCH"
working-directory: /tmp/schemastore
- name: Create or update pull request
if: steps.diff.outputs.changed == 'true'
run: |
if ! gh pr view "$BRANCH" --repo SchemaStore/schemastore > /dev/null 2>&1; then
gh pr create \
--repo SchemaStore/schemastore \
--title "Update tox JSON Schema to ${{ github.ref_name }}" \
--body "Updates tox's JSON Schema to [${{ github.sha }}](https://github.com/tox-dev/tox/commit/${{ github.sha }}) (release ${{ github.ref_name }})."
else
gh pr edit "$BRANCH" --repo SchemaStore/schemastore \
--title "Update tox JSON Schema to ${{ github.ref_name }}" \
--body "Updates tox's JSON Schema to [${{ github.sha }}](https://github.com/tox-dev/tox/commit/${{ github.sha }}) (release ${{ github.ref_name }})."
fi
working-directory: /tmp/schemastore
3 changes: 3 additions & 0 deletions docs/changelog/1388.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Enhance ``tox schema`` command: add ``x-taplo`` metadata for IDE integration, product dict support for ``env_list``,
handle ``int`` and ``PythonConstraints`` types, fix ``$schema`` draft-07 URI, and add schema freshness test. Add
``tox.toml`` to SchemaStore catalog for automatic IDE validation - by :user:`gaborbernat`.
89 changes: 77 additions & 12 deletions src/tox/session/cmd/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ def gen_schema(state: State) -> int:
core = state.conf.core
strict = state.conf.options.strict

# Accessing this adds extra stuff to core, so we need to do it first
env_properties = _get_schema(state.envs["py"].conf, path="#/properties/env_run_base/properties")
# Use any available run environment for introspection (fall back to "py" which is always defined)
env_name = next(state.envs.iter(only_active=False), "py")
env_properties = _get_schema(state.envs[env_name].conf, path="#/properties/env_run_base/properties")

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

Expand All @@ -55,23 +56,41 @@ def gen_schema(state: State) -> int:
"properties": _get_schema(conf, path=f"#/properties/{key}/properties"),
}

docs_base = "https://tox.wiki/en/stable"
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",
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/tox-dev/tox/main/src/tox/tox.schema.json",
"title": "tox configuration",
"description": "tox configuration file (tox.toml or [tool.tox] in pyproject.toml)",
"x-taplo": {"links": {"key": f"{docs_base}/config.html"}},
"type": "object",
"properties": {
**properties,
"env_run_base": {
"type": "object",
"description": "base configuration for run environments",
"x-taplo": {"links": {"key": f"{docs_base}/config.html#run-environment"}},
"properties": env_properties,
"additionalProperties": not strict,
},
"env_pkg_base": {
"type": "object",
"$ref": "#/properties/env_run_base",
"description": "base configuration for packaging environments",
"x-taplo": {"links": {"key": f"{docs_base}/config.html#packaging-environment"}},
"additionalProperties": not strict,
},
"env": {"type": "object", "patternProperties": {"^.*$": {"$ref": "#/properties/env_run_base"}}},
"legacy_tox_ini": {"type": "string"},
"env": {
"type": "object",
"description": "per-environment overrides (keyed by environment name)",
"x-taplo": {"links": {"key": f"{docs_base}/config.html#run-environment"}},
"patternProperties": {"^.*$": {"$ref": "#/properties/env_run_base"}},
},
"legacy_tox_ini": {
"type": "string",
"description": "tox configuration in INI format embedded in a TOML file",
"x-taplo": {"links": {"key": f"{docs_base}/config.html#pyproject-toml-ini"}},
},
},
"additionalProperties": not strict,
"definitions": {
Expand Down Expand Up @@ -119,29 +138,33 @@ def gen_schema(state: State) -> int:


def _get_schema(conf: ConfigSet, path: str) -> dict[str, dict[str, typing.Any]]:
properties = {}
properties: dict[str, dict[str, typing.Any]] = {}
for x in conf.get_configs():
name, *aliases = x.keys
of_type = getattr(x, "of_type", None)
if of_type is None:
if (of_type := getattr(x, "of_type", None)) 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}"}
properties[alias] = {
"$ref": f"{path}/{name}",
"description": f"Deprecated: use {name!r} instead",
"deprecated": True,
}
return properties


def _process_type(of_type: typing.Any) -> dict[str, typing.Any]: # noqa: C901, PLR0911
def _process_type(of_type: typing.Any) -> dict[str, typing.Any]: # noqa: C901, PLR0911, PLR0912
if of_type in {
Path,
str,
packaging.version.Version,
packaging.requirements.Requirement,
tox.tox_env.python.pip.req_file.PythonDeps,
tox.tox_env.python.pip.req_file.PythonConstraints,
}:
return {"type": "string"}
if typing.get_origin(of_type) is typing.Union:
Expand All @@ -152,11 +175,53 @@ def _process_type(of_type: typing.Any) -> dict[str, typing.Any]: # noqa: C901,
raise ValueError(msg)
if of_type is bool:
return {"type": "boolean"}
if of_type is int:
return {"type": "integer", "minimum": 0}
if of_type is float:
return {"type": "number"}
if typing.get_origin(of_type) is typing.Literal:
return {"enum": list(typing.get_args(of_type))}
if of_type in {tox.config.types.Command, tox.config.types.EnvList}:
if of_type is tox.config.types.EnvList:
return {
"type": "array",
"items": {
"oneOf": [
{"$ref": "#/definitions/subs"},
{
"type": "object",
"required": ["product"],
"properties": {
"product": {
"type": "array",
"items": {
"oneOf": [
{"type": "array", "items": {"type": "string"}},
{
"type": "object",
"required": ["prefix"],
"properties": {
"prefix": {"type": "string"},
"start": {"type": "integer"},
"stop": {"type": "integer"},
},
"additionalProperties": False,
},
],
},
"description": "factor groups for cartesian product expansion",
},
"exclude": {
"type": "array",
"items": {"type": "string"},
"description": "environment names to exclude from product",
},
},
"additionalProperties": False,
},
],
},
}
if of_type is tox.config.types.Command:
return {"type": "array", "items": {"$ref": "#/definitions/subs"}}
if typing.get_origin(of_type) in {list, set}:
if typing.get_args(of_type)[0] in {str, packaging.requirements.Requirement}:
Expand Down
Loading