From 992602280c73642fece6daf865c9774e7bb92e62 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Wed, 8 Jul 2026 10:47:49 -0500 Subject: [PATCH] add top-level --- conda_recipe_v2_schema/model.py | 39 +++++++++++++++++++++--- examples/invalid/dollarschema.yaml | 1 + schema.json | 49 ++++++++++++++++++++++++------ tests/test_recipe.py | 5 +-- 4 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 examples/invalid/dollarschema.yaml diff --git a/conda_recipe_v2_schema/model.py b/conda_recipe_v2_schema/model.py index 142172f..6b96f78 100644 --- a/conda_recipe_v2_schema/model.py +++ b/conda_recipe_v2_schema/model.py @@ -1,7 +1,8 @@ from __future__ import annotations import json -from typing import Annotated, Any, Generic, Literal, TypeVar, Union +import sys +from typing import Annotated, Any, ClassVar, Generic, Literal, TypeVar, Union from pydantic import ( AnyHttpUrl, @@ -13,6 +14,19 @@ constr, ) +SCHEMA_DRAFT = "http://json-schema.org/draft-07/schema#" +SCHEMA_URI = ( + "https://raw.githubusercontent.com/prefix-dev/recipe-format/refs/heads/main/schema.json" +) +SCHEMA_HTML = "https://github.com/prefix-dev/recipe-format" + +SCHEMA_HEADER = { + "$id": SCHEMA_URI, + "$schema": SCHEMA_DRAFT, + "title": "conda recipe file", +} + + NonEmptyStr = constr(min_length=1) PathNoBackslash = constr(pattern=r"^[^\\]+$") UnsignedInt = conint(ge=0) @@ -21,7 +35,7 @@ class StrictBaseModel(BaseModel): - model_config = ConfigDict(extra="forbid") + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") ########################### @@ -704,6 +718,14 @@ class Output(StrictBaseModel): class BaseRecipe(StrictBaseModel): + schema_: str | None = Field( + SCHEMA_URI, + alias="$schema", + title="Schema", + description="An identifier for the schema used to validate this recipe", + json_schema_extra={"format": "uri-reference"}, + ) + schema_version: SchemaVersion = Field( 1, description="The version of the YAML schema for a recipe. If the version is omitted it is assumed to be 1.", @@ -771,9 +793,16 @@ class SimpleRecipe(BaseRecipe): Recipe = TypeAdapter(SimpleRecipe | ComplexRecipe) -def main(): - print(json.dumps(Recipe.json_schema(), indent=2)) +def top_level_schema() -> dict[str, Any]: + schema = Recipe.json_schema() + any_of = schema.pop("anyOf") + return {**SCHEMA_HEADER, "oneOf": any_of, **schema} + + +def main() -> int: + print(json.dumps(top_level_schema(), indent=2)) + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/examples/invalid/dollarschema.yaml b/examples/invalid/dollarschema.yaml new file mode 100644 index 0000000..dc1de5e --- /dev/null +++ b/examples/invalid/dollarschema.yaml @@ -0,0 +1 @@ +$schema: NO diff --git a/schema.json b/schema.json index d113b3f..e5dcb41 100644 --- a/schema.json +++ b/schema.json @@ -1,4 +1,15 @@ { + "$id": "https://raw.githubusercontent.com/prefix-dev/recipe-format/refs/heads/main/schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "conda recipe file", + "oneOf": [ + { + "$ref": "#/$defs/SimpleRecipe" + }, + { + "$ref": "#/$defs/ComplexRecipe" + } + ], "$defs": { "About": { "additionalProperties": false, @@ -809,6 +820,20 @@ "ComplexRecipe": { "additionalProperties": false, "properties": { + "$schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "https://raw.githubusercontent.com/prefix-dev/recipe-format/refs/heads/main/schema.json", + "description": "An identifier for the schema used to validate this recipe", + "format": "uri-reference", + "title": "Schema" + }, "schema_version": { "default": 1, "description": "The version of the YAML schema for a recipe. If the version is omitted it is assumed to be 1.", @@ -4092,6 +4117,20 @@ "SimpleRecipe": { "additionalProperties": false, "properties": { + "$schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "https://raw.githubusercontent.com/prefix-dev/recipe-format/refs/heads/main/schema.json", + "description": "An identifier for the schema used to validate this recipe", + "format": "uri-reference", + "title": "Schema" + }, "schema_version": { "default": 1, "description": "The version of the YAML schema for a recipe. If the version is omitted it is assumed to be 1.", @@ -4916,13 +4955,5 @@ "title": "Variant", "type": "object" } - }, - "anyOf": [ - { - "$ref": "#/$defs/SimpleRecipe" - }, - { - "$ref": "#/$defs/ComplexRecipe" - } - ] + } } diff --git a/tests/test_recipe.py b/tests/test_recipe.py index 51c0c54..9538393 100644 --- a/tests/test_recipe.py +++ b/tests/test_recipe.py @@ -6,7 +6,7 @@ from jsonschema.exceptions import ValidationError from pydantic import ValidationError as PydanticValidationError -from conda_recipe_v2_schema.model import Recipe +from conda_recipe_v2_schema.model import Recipe, top_level_schema @pytest.fixture( @@ -25,6 +25,7 @@ def valid_recipe(request) -> str: scope="module", params=[ "complex1", + "dollarschema", "simple1", ], ) @@ -53,7 +54,7 @@ def test_recipe_schema_invalid(recipe_schema, invalid_recipe): def test_recipe_schema_not_changed(recipe_schema): - assert recipe_schema == Recipe.json_schema() + assert recipe_schema == top_level_schema() def test_patches_valid_conditional():