-
Notifications
You must be signed in to change notification settings - Fork 37
test_json_schema #452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
test_json_schema #452
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,3 +54,4 @@ dmypy.json | |
| /docs/build | ||
|
|
||
| .venv/ | ||
| uv.lock | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ def __init__(self) -> None: | |
| self._patchers: list[Patcher] = [] | ||
|
|
||
| def _append_with_patcher(self: S, patcher: Callable[[JSONSchema], JSONSchema]) -> S: | ||
| self_copy = copy(self) | ||
| self_copy = deepcopy(self) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why copy patcher objects? They should be immutable
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The actual issue was list aliasing on _patchers after the shallow copy(). It was same for previous and new item. Use list for fix it, not append |
||
| self_copy._patchers.append(patcher) | ||
| return self_copy | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,276 @@ | ||
| from dataclasses import dataclass | ||
| from typing import Any, Optional | ||
|
|
||
| import pytest | ||
| from tests_helpers.structure_tools import EXISTS, NOT_EXISTS, assert_structure | ||
|
|
||
| from adaptix import Retort | ||
| from adaptix._internal.definitions import Direction | ||
| from adaptix._internal.morphing.facade.func import ( | ||
| generate_json_schema, | ||
| generate_json_schemas_namespace, | ||
| load_json_schema, | ||
| ) | ||
| from adaptix._internal.morphing.json_schema.definitions import JSONSchema | ||
| from adaptix._internal.morphing.json_schema.mangling import IndexRefMangler | ||
| from adaptix._internal.morphing.json_schema.ref_generator import BuiltinRefGenerator | ||
| from adaptix._internal.morphing.json_schema.resolver import BuiltinJSONSchemaResolver | ||
| from adaptix._internal.morphing.json_schema.schema_model import ( | ||
| JSONSchemaBuiltinFormat, | ||
| JSONSchemaType, | ||
| ) | ||
| from adaptix.load_error import AggregateLoadError | ||
|
|
||
| DIALECT_URI = "https://json-schema.org/draft/2020-12/schema" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This constant exists in main library code
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! fixed |
||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ["raw", "expected"], | ||
| [ | ||
| ("null", JSONSchemaType.NULL), | ||
| ("boolean", JSONSchemaType.BOOLEAN), | ||
| ("object", JSONSchemaType.OBJECT), | ||
| ("array", JSONSchemaType.ARRAY), | ||
| ("number", JSONSchemaType.NUMBER), | ||
| ("integer", JSONSchemaType.INTEGER), | ||
| ("string", JSONSchemaType.STRING), | ||
| ], | ||
| ) | ||
| def test_load_json_schema_type_enum_values(raw: str, expected: JSONSchemaType): | ||
| schema = load_json_schema({"type": raw}) | ||
|
|
||
| assert schema.type == expected | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ["schema_data", "expected"], | ||
| [ | ||
| ( | ||
| {"type": "string", "title": "MyStr", "description": "A string"}, | ||
| JSONSchema(type=JSONSchemaType.STRING, title="MyStr", description="A string"), | ||
| ), | ||
| ( | ||
| {"type": "string", "format": "date-time"}, | ||
| JSONSchema(type=JSONSchemaType.STRING, format=JSONSchemaBuiltinFormat.DATE_TIME), | ||
| ), | ||
| ( | ||
| {"type": "string", "format": "my-custom-format"}, | ||
| JSONSchema(type=JSONSchemaType.STRING, format="my-custom-format"), | ||
| ), | ||
| ( | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "name": {"type": "string"}, | ||
| "age": {"type": "integer"}, | ||
| }, | ||
| "required": ["name"], | ||
| }, | ||
| JSONSchema( | ||
| type=JSONSchemaType.OBJECT, | ||
| properties={ | ||
| "name": JSONSchema(type=JSONSchemaType.STRING), | ||
| "age": JSONSchema(type=JSONSchemaType.INTEGER), | ||
| }, | ||
| required=["name"], | ||
| ), | ||
| ), | ||
| ( | ||
| {"anyOf": [{"type": "string"}, {"type": "integer"}]}, | ||
| JSONSchema(any_of=[ | ||
| JSONSchema(type=JSONSchemaType.STRING), | ||
| JSONSchema(type=JSONSchemaType.INTEGER), | ||
| ]), | ||
| ), | ||
| ], | ||
| ) | ||
| def test_load_json_schema_parses_dict_to_object(schema_data, expected): | ||
| schema = load_json_schema(schema_data) | ||
|
|
||
| assert schema == expected | ||
|
|
||
|
|
||
| def test_load_json_schema_strict_unknown_field_raises(): | ||
| with pytest.raises(AggregateLoadError): | ||
| load_json_schema({"type": "string", "x-custom": "value"}, error_on_extra=True) | ||
|
|
||
|
|
||
| def test_load_json_schema_lax_unknown_field_goes_to_extra_keywords(): | ||
| schema = load_json_schema({"type": "string", "x-custom": "value"}, error_on_extra=False) | ||
|
|
||
| assert schema.extra_keywords == {"x-custom": "value"} | ||
|
|
||
|
|
||
| def test_load_json_schema_empty_dict(): | ||
| schema = load_json_schema({}) | ||
|
|
||
| assert schema == JSONSchema() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are these tests for? They check Adaptix's basic model loading functionality
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point - trimmed the tests that only exercised generic Adaptix dataclass/enum loading, since that's already covered by core Adaptix tests. Kept only what's actually specific to this facade's retort config in func.py: the custom _format_loader and the error_on_extra behavior (ExtraForbid vs extra_out="extra_keywords") - that logic is unique to load_json_schema and isn't tested elsewhere. |
||
|
|
||
|
|
||
| @dataclass | ||
| class SimpleModel: | ||
| name: str | ||
| value: int | ||
|
|
||
|
|
||
| @dataclass | ||
| class ModelWithOptional: | ||
| required_field: str | ||
| optional_field: Optional[str] = None | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ["with_dialect_uri", "expected_schema"], | ||
| [ | ||
| (True, DIALECT_URI), | ||
| (False, NOT_EXISTS), | ||
| ], | ||
| ids=["with_uri", "without_uri"], | ||
| ) | ||
| def test_generate_json_schema_dialect_uri(with_dialect_uri: bool, expected_schema: Any): # noqa: FBT001 | ||
| schema = generate_json_schema(Retort(), SimpleModel, Direction.INPUT, with_dialect_uri=with_dialect_uri) | ||
|
|
||
| assert_structure(schema, {"$schema": expected_schema}) | ||
|
|
||
|
|
||
| def test_generate_json_schema_top_level_keys(): | ||
| schema = generate_json_schema(Retort(), SimpleModel, Direction.INPUT) | ||
|
|
||
| assert_structure(schema, { | ||
| "$schema": EXISTS, | ||
| "$ref": EXISTS, | ||
| "$defs": { | ||
| "SimpleModel": EXISTS, | ||
| }, | ||
| }, strict=True) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why these tests checks only keys?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Switched this one to assert_morphing. test_generate_json_schema_dialect_uri just check $schema - remove assert_structure from file |
||
|
|
||
|
|
||
| def test_generate_json_schema_custom_ref_prefix(): | ||
| schema = generate_json_schema( | ||
| Retort(), SimpleModel, Direction.INPUT, | ||
| local_ref_prefix="#/components/schemas/", | ||
| ) | ||
|
|
||
| assert_structure(schema, {"$ref": "#/components/schemas/SimpleModel"}) | ||
|
|
||
|
|
||
| def test_generate_json_schema_occupied_refs_triggers_mangling(): | ||
| schema = generate_json_schema( | ||
| Retort(), SimpleModel, Direction.INPUT, | ||
| occupied_refs=("SimpleModel",), | ||
| resolver=BuiltinJSONSchemaResolver(BuiltinRefGenerator(), IndexRefMangler()), | ||
| ) | ||
|
|
||
| assert_structure(schema, { | ||
| "$defs": { | ||
| "SimpleModel": NOT_EXISTS, | ||
| "SimpleModel-1": EXISTS, | ||
| }, | ||
| }) | ||
|
|
||
|
|
||
| def test_generate_json_schema_custom_resolver(): | ||
| schema = generate_json_schema( | ||
| Retort(), SimpleModel, Direction.INPUT, | ||
| resolver=BuiltinJSONSchemaResolver(BuiltinRefGenerator(), IndexRefMangler()), | ||
| ) | ||
|
|
||
| assert_structure(schema, {"$defs": {"SimpleModel": EXISTS}}) | ||
|
|
||
|
|
||
| def test_generate_json_schema_optional_field_not_required_on_input(): | ||
| input_schema = generate_json_schema(Retort(), ModelWithOptional, Direction.INPUT) | ||
| output_schema = generate_json_schema(Retort(), ModelWithOptional, Direction.OUTPUT) | ||
|
|
||
| # Input schema: only required_field is required (optional_field has default) | ||
| assert_structure(input_schema, { | ||
| "$defs": { | ||
| "ModelWithOptional": { | ||
| "required": ["required_field"], | ||
| }, | ||
| }, | ||
| }) | ||
|
|
||
| # Output schema: both fields are required (dumping doesn't care about defaults) | ||
| assert_structure(output_schema, { | ||
| "$defs": { | ||
| "ModelWithOptional": { | ||
| "required": ["required_field", "optional_field"], | ||
| }, | ||
| }, | ||
| }) | ||
|
|
||
|
|
||
| @dataclass | ||
| class Tag: | ||
| label: str | ||
|
|
||
|
|
||
| @dataclass | ||
| class Article: | ||
| title: str | ||
| tag: Tag | ||
|
|
||
|
|
||
| @dataclass | ||
| class Post: | ||
| body: str | ||
| tag: Tag | ||
|
|
||
|
|
||
| def _namespace_query(): | ||
| retort = Retort() | ||
| return [(retort, Direction.INPUT, Article), (retort, Direction.INPUT, Post)] | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "resolver", | ||
| [ | ||
| pytest.param(None, id="default"), | ||
| pytest.param( | ||
| BuiltinJSONSchemaResolver(BuiltinRefGenerator(), IndexRefMangler()), | ||
| id="custom_resolver", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_generate_schemas_namespace_deduplicates_shared_types(resolver): | ||
| query = _namespace_query() | ||
| kwargs = {"resolver": resolver} if resolver else {} | ||
|
|
||
| defs, schemas = generate_json_schemas_namespace(query, **kwargs) | ||
|
|
||
| assert len(schemas) == 2 | ||
|
|
||
| assert_structure(defs, { | ||
| "Article": EXISTS, | ||
| "Post": EXISTS, | ||
| "Tag": EXISTS, | ||
| }, strict=True) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "with_dialect_uri", | ||
| [ | ||
| pytest.param(True, id="with_uri"), | ||
| pytest.param(False, id="without_uri"), | ||
| ], | ||
| ) | ||
| def test_generate_schemas_namespace_dialect_uri(with_dialect_uri: bool): # noqa: FBT001 | ||
| query = _namespace_query() | ||
| _, schemas = generate_json_schemas_namespace(query, with_dialect_uri=with_dialect_uri) | ||
|
|
||
| expected_schema = DIALECT_URI if with_dialect_uri else NOT_EXISTS | ||
| for schema in schemas: | ||
| assert_structure(schema, {"$schema": expected_schema, "$ref": EXISTS}) | ||
|
|
||
|
|
||
| def test_generate_schemas_namespace_custom_ref_prefix(): | ||
| query = _namespace_query() | ||
| _, schemas = generate_json_schemas_namespace( | ||
| query, local_ref_prefix="#/components/schemas/", | ||
| ) | ||
|
|
||
| assert_structure(schemas, [ | ||
| {"$ref": "#/components/schemas/Article"}, | ||
| {"$ref": "#/components/schemas/Post"}, | ||
| ]) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why custom loader is required here? Why data for unkown types are directly returned?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Custom loader needed because format: JSONSchemaBuiltinFormat | str is ambiguous for the default union loader (both sides are string-shaped, so plain str would always win and the enum would never be produced) - enum is tried first, falling back to the raw string for custom formats (see test at test_json_schema_facade.py, test test_load_json_schema_format_uses_builtin_enum_or_falls_back_to_str with "format": "date-time"). Good catch on the fallthrough for non-str data - fixed to raise TypeLoadError.