Download source/tests/test_schema.py from gcoderw/DEG-5B-E2B-1019: direct link, hf CLI and curl.
- Browser
- Download file 17.5 kB
-
https://huggingface.co/gcoderw/DEG-5B-E2B-1019/resolve/main/source/tests/test_schema.py
- Command line
-
hf download hf://gcoderw/DEG-5B-E2B-1019/source/tests/test_schema.py
-
curl -L -o test_schema.py https://huggingface.co/gcoderw/DEG-5B-E2B-1019/resolve/main/source/tests/test_schema.py
17.5 kB
| """Schema -> fill plan contract (SPEC.md §3-§4).""" | |
| from __future__ import annotations | |
| import copy | |
| import json | |
| import pytest | |
| from deg_cases import ARRAYS, INVOICE, NESTED, PRODUCT_LISTING, SUPPORT_TICKET | |
| from deg.schema import ITEM, SchemaError, compile_schema, pointer | |
| def modes(plan): | |
| return [(f.pointer, f.mode) for f in plan.fields] | |
| def obj(**props): | |
| return {"type": "object", "properties": props, "required": list(props)} | |
| def errors_of(schema): | |
| with pytest.raises(SchemaError) as info: | |
| compile_schema(schema) | |
| return info.value.errors | |
| # ----------------------------------------------------------------------------- examples | |
| def test_support_ticket_plan(): | |
| plan = compile_schema(SUPPORT_TICKET) | |
| assert modes(plan) == [ | |
| ("/department", "decide.choice"), | |
| ("/urgent", "decide.noul"), | |
| ("/anger", "decide.score"), | |
| ("/customer_email", "extract.span"), | |
| ("/order_id", "extract.span"), | |
| ("/reply", "generate"), | |
| ] | |
| dept, _, anger, email, order, reply = plan.fields | |
| assert dept.options == ("billing", "technical", "sales") | |
| assert dept.description == "Which team should handle this?" | |
| assert anger.levels == (0, 1, 2, 3, 4) | |
| assert (email.format, email.nullable, email.on_null) == ("email", True, "null") | |
| assert (order.on_null, order.constraints) == ("omit", {"pattern": "^[A-Z]{2}-\\d+$"}) | |
| assert reply.constraints == {"maxLength": 400} and reply.on_null == "forbid" | |
| assert [f.pointer for f in plan.generate_fields] == ["/reply"] | |
| assert len(plan.parallel_fields) == 5 | |
| def test_invoice_plan_refs_oneof_and_arrays(): | |
| plan = compile_schema(INVOICE) | |
| assert modes(plan) == [ | |
| ("/invoice_number", "extract.span"), | |
| ("/issued", "extract.date"), | |
| ("/due", "extract.date"), | |
| ("/vendor/name", "extract.span"), | |
| ("/vendor/website", "extract.span"), | |
| ("/currency", "decide.choice"), | |
| ("/line_items/*/description", "extract.span"), | |
| ("/line_items/*/quantity", "extract.number"), | |
| ("/line_items/*/amount", "extract.number"), | |
| ("/total", "extract.number"), | |
| ] | |
| currency = plan.field("/currency") | |
| assert currency.options == ("USD", "EUR", "GBP") | |
| assert currency.option_descriptions == ("US dollars", "Euros", None) | |
| amount = plan.field("/line_items/*/amount") | |
| assert amount.path == ("line_items", ITEM, "amount") | |
| assert amount.description == "Amount in invoice currency" # from the $ref target | |
| assert amount.constraints == {"minimum": 0} | |
| assert plan.field("/line_items/*/quantity").on_null == "omit" | |
| assert plan.field("/due").on_null == "null" | |
| assert plan.field("/vendor/website").format == "uri" | |
| (items,) = plan.arrays | |
| assert (items.pointer, items.min_items, items.max_items) == ("/line_items", 1, 50) | |
| assert plan.generate_fields == () | |
| def test_product_listing_generates_only_marked_fields(): | |
| plan = compile_schema(PRODUCT_LISTING) | |
| assert modes(plan) == [ | |
| ("/title", "extract.span"), | |
| ("/category", "decide.choice"), | |
| ("/condition", "decide.choice"), | |
| ("/price", "extract.number"), # one-sided bound: a quantity, extracted | |
| ("/rating", "extract.number"), # a number: always extracted; bounds only validate | |
| ("/in_stock", "decide.noul"), | |
| ("/description", "generate"), | |
| ("/tags/*", "generate"), # x-generate on the array applies to its string items | |
| ] | |
| rating = plan.field("/rating") | |
| assert rating.levels is None and rating.constraints == {"minimum": 0, "maximum": 5} | |
| assert [f.pointer for f in plan.generate_fields] == ["/description", "/tags/*"] | |
| assert plan.field("/tags/*").constraints == {"maxLength": 24} | |
| assert plan.arrays[0].max_items == 5 | |
| def test_nested_objects(): | |
| plan = compile_schema(NESTED) | |
| assert modes(plan) == [ | |
| ("/patient/name", "extract.span"), | |
| ("/patient/birth", "extract.date"), | |
| ("/patient/contact/email", "extract.span"), | |
| ("/patient/contact/id", "extract.span"), | |
| ("/visit/at", "extract.date"), | |
| ("/visit/follow_up", "decide.choice"), # nullable boolean -> 3-way choice | |
| ("/visit/summary", "generate"), | |
| ] | |
| assert plan.field("/visit/follow_up").options == (True, False, None) | |
| assert plan.field("/visit/at").format == "date-time" | |
| assert plan.field("/patient/contact/id").format == "uuid" | |
| def test_arrays_per_item(): | |
| plan = compile_schema(ARRAYS) | |
| assert modes(plan) == [ | |
| ("/labels/*", "decide.choice"), | |
| ("/people/*/name", "extract.span"), | |
| ("/people/*/aliases/*", "extract.span"), | |
| ("/matrix/*/*", "extract.number"), | |
| ] | |
| assert [a.pointer for a in plan.arrays] == [ | |
| "/labels", "/people", "/people/*/aliases", "/matrix", "/matrix/*", | |
| ] | |
| assert plan.arrays[0].unique_items is True | |
| def test_type_to_mode_table(): | |
| plan = compile_schema(obj( | |
| e={"enum": [1, 2, 3]}, | |
| mixed={"enum": ["a", 1, None]}, | |
| b={"type": "boolean"}, | |
| i_small={"type": "integer", "minimum": 1, "maximum": 10}, | |
| i_step={"type": "integer", "minimum": 0, "maximum": 100, "multipleOf": 25}, | |
| i_excl={"type": "integer", "exclusiveMinimum": 0, "exclusiveMaximum": 4}, | |
| i_wide={"type": "integer", "minimum": 0, "maximum": 150}, | |
| i_open={"type": "integer"}, | |
| n_unit={"type": "number", "minimum": 0, "maximum": 1}, | |
| n_excl={"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1}, | |
| i_12={"type": "integer", "minimum": 0, "maximum": 11}, | |
| n_open={"type": "number"}, | |
| s={"type": "string"}, | |
| d={"type": "string", "format": "date"}, | |
| dt={"type": "string", "format": "date-time"}, | |
| u={"type": "string", "format": "uuid"}, | |
| g={"type": "string", "x-generate": True}, | |
| g_false={"type": "string", "x-generate": False}, | |
| )) | |
| got = {f.pointer: (f.mode, f.levels, f.options) for f in plan.fields} | |
| assert got["/e"] == ("decide.choice", None, (1, 2, 3)) | |
| assert got["/mixed"] == ("decide.choice", None, ("a", 1, None)) | |
| assert got["/b"][0] == "decide.noul" | |
| assert got["/i_small"] == ("decide.score", tuple(range(1, 11)), None) | |
| assert got["/i_step"] == ("decide.score", (0, 25, 50, 75, 100), None) | |
| assert got["/i_excl"] == ("decide.score", (1, 2, 3), None) | |
| assert got["/i_wide"][0] == "extract.number" | |
| assert got["/i_open"][0] == "extract.number" | |
| assert got["/n_unit"][0] == "extract.number" | |
| assert got["/n_excl"][0] == "extract.number" | |
| assert got["/i_12"][0] == "extract.number" # 12 admissible values: extracted | |
| assert got["/n_open"][0] == "extract.number" | |
| assert got["/s"][0] == "extract.span" | |
| assert got["/d"][0] == got["/dt"][0] == "extract.date" | |
| assert got["/u"][0] == "extract.span" | |
| assert got["/g"][0] == "generate" | |
| assert got["/g_false"][0] == "extract.span" | |
| assert plan.field("/mixed").nullable is True | |
| def test_nullable_forms(): | |
| plan = compile_schema(obj( | |
| a={"type": ["string", "null"]}, | |
| b={"anyOf": [{"type": "string"}, {"type": "null"}], "description": "B"}, | |
| c={"oneOf": [{"type": "null"}, {"type": "integer", "minimum": 0, "maximum": 3}]}, | |
| d={"anyOf": [{"enum": ["x", "y"]}, {"type": "null"}]}, | |
| e={"type": ["string", "null"], "enum": ["x", "y", None]}, | |
| )) | |
| a, b, c, d, e = plan.fields | |
| assert a.nullable and b.nullable and b.description == "B" | |
| assert (c.mode, c.levels, c.null_outcome) == ("decide.score", (0, 1, 2, 3), True) | |
| assert d.options == ("x", "y", None) and d.nullable | |
| assert e.options == ("x", "y", None) | |
| def test_optional_fields_and_on_null(): | |
| plan = compile_schema({ | |
| "type": "object", | |
| "properties": {"req": {"type": "string"}, "opt": {"type": "string"}, | |
| "decided": {"type": "boolean"}}, | |
| "required": ["req"], | |
| }) | |
| assert [f.on_null for f in plan.fields] == ["forbid", "omit", "omit"] | |
| assert [f.required for f in plan.fields] == [True, False, False] | |
| def test_pointer_escaping(): | |
| assert pointer(("a/b", "c~d", ITEM, "*")) == "/a~1b/c~0d/*/~2" | |
| plan = compile_schema(obj(**{"a/b": {"type": "string"}})) | |
| assert plan.fields[0].pointer == "/a~1b" | |
| def test_local_ref_with_annotation_override(): | |
| schema = { | |
| "type": "object", | |
| "definitions": {"name": {"type": "string", "description": "orig"}}, | |
| "properties": {"n": {"$ref": "#/definitions/name", "description": "override"}}, | |
| } | |
| (field,) = compile_schema(schema).fields | |
| assert (field.mode, field.description) == ("extract.span", "override") | |
| # ----------------------------------------------------------------------------- errors | |
| def test_x_generate_only_on_string(bad): | |
| (error,) = errors_of(obj(f=bad)) | |
| assert "x-generate is allowed only on string or array of string" in error["msg"] | |
| def test_x_generate_with_format_and_bad_value(): | |
| (error,) = errors_of(obj(f={"type": "string", "format": "email", "x-generate": True})) | |
| assert "format" in error["msg"] | |
| (error,) = errors_of(obj(f={"type": "string", "x-generate": "yes"})) | |
| assert error["msg"] == "x-generate must be a boolean" | |
| def test_unsupported_keywords(bad, keyword): | |
| errors = errors_of(obj(f=bad)) | |
| assert any(f"unsupported keyword {keyword!r}" in e["msg"] for e in errors), errors | |
| def test_validation_errors(bad, fragment): | |
| errors = errors_of(obj(f=bad)) | |
| assert any(fragment in e["msg"] for e in errors), errors | |
| assert all(e["loc"][:3] == ["schema", "properties", "f"] for e in errors), errors | |
| def test_draft07_tuple_items_rejected(): | |
| schema = {"$schema": "http://json-schema.org/draft-07/schema#", **obj( | |
| f={"type": "array", "items": [{"type": "string"}]})} | |
| (error,) = errors_of(schema) | |
| assert "tuple arrays" in error["msg"] | |
| def test_recursive_ref_rejected(): | |
| schema = { | |
| "type": "object", | |
| "$defs": {"node": {"type": "object", "properties": {"child": {"$ref": "#/$defs/node"}}}}, | |
| "properties": {"root": {"$ref": "#/$defs/node"}}, | |
| } | |
| errors = errors_of(schema) | |
| assert any("recursive $ref" in e["msg"] for e in errors) | |
| def test_root_must_be_object_and_errors_are_collected(): | |
| assert "type: object" in errors_of({"type": "string"})[0]["msg"] | |
| errors = errors_of(obj(a={"type": "boolean", "x-generate": True}, | |
| b={"allOf": [{"type": "string"}]}, | |
| c={"type": "string", "format": "ipv4"})) | |
| assert sorted(e["loc"][2] for e in errors) == ["a", "b", "c"] | |
| with pytest.raises(SchemaError): | |
| compile_schema("not a schema") | |
| with pytest.raises(SchemaError): | |
| compile_schema({"type": "object", "properties": {"a": {"type": 5}}}) # invalid JSON Schema | |
| def test_required_must_name_properties(): | |
| errors = errors_of({"type": "object", "properties": {}, "required": ["ghost"]}) | |
| assert "not in properties" in errors[0]["msg"] | |
| # ----------------------------------------------------------------------------- stability | |
| def test_plan_is_deterministic_and_does_not_mutate_schema(): | |
| for schema in (SUPPORT_TICKET, INVOICE, PRODUCT_LISTING, NESTED, ARRAYS): | |
| before = copy.deepcopy(schema) | |
| a, b = compile_schema(schema), compile_schema(copy.deepcopy(schema)) | |
| assert a.to_dict() == b.to_dict() | |
| assert json.dumps(a.to_dict(), sort_keys=True) == json.dumps(b.to_dict(), sort_keys=True) | |
| assert schema == before | |
| def _reordered(schema, order): | |
| out = copy.deepcopy(schema) | |
| out["properties"] = {k: schema["properties"][k] for k in order} | |
| return out | |
| def test_parallel_fields_are_order_independent(): | |
| shuffled = _reordered(SUPPORT_TICKET, ["reply", "order_id", "anger", "urgent", | |
| "customer_email", "department"]) | |
| a, b = compile_schema(SUPPORT_TICKET), compile_schema(shuffled) | |
| fa = {f.pointer: f.fingerprint for f in a.fields} | |
| fb = {f.pointer: f.fingerprint for f in b.fields} | |
| assert fa == fb | |
| assert a.fingerprint == b.fingerprint # one generate field: order of the rest is moot | |
| def test_generate_order_is_meaningful(): | |
| schema = obj( | |
| kind={"enum": ["a", "b"]}, | |
| first={"type": "string", "x-generate": True}, | |
| second={"type": "string", "x-generate": True}, | |
| ) | |
| swapped = _reordered(schema, ["second", "kind", "first"]) | |
| a, b = compile_schema(schema), compile_schema(swapped) | |
| assert [f.pointer for f in a.generate_fields] == ["/first", "/second"] | |
| assert [f.pointer for f in b.generate_fields] == ["/second", "/first"] | |
| assert a.fingerprint != b.fingerprint | |
| moved_parallel = _reordered(schema, ["first", "kind", "second"]) | |
| assert compile_schema(moved_parallel).fingerprint == a.fingerprint | |
| def test_field_fingerprint_independent_of_composition(): | |
| """A leaf's spec is the same alone, next to other fields, or merged into a bigger schema.""" | |
| alone = compile_schema(obj(department=SUPPORT_TICKET["properties"]["department"])) | |
| merged = compile_schema(obj( | |
| department=SUPPORT_TICKET["properties"]["department"], | |
| **{k: v for k, v in PRODUCT_LISTING["properties"].items()}, | |
| )) | |
| assert alone.fields[0].fingerprint == merged.field("/department").fingerprint | |
| product = compile_schema(PRODUCT_LISTING) | |
| for f in product.parallel_fields: | |
| g = merged.field(f.pointer) | |
| # required flags match because obj() requires everything, as PRODUCT_LISTING does | |
| assert g.fingerprint == f.fingerprint | |
| def test_x_levels_and_x_max_tokens_extensions(): | |
| ok = compile_schema({"type": "object", "properties": { | |
| "s": {"type": "integer", "minimum": 1, "maximum": 3, "x-levels": {"1": "low", "3": "high"}}, | |
| "g": {"type": "string", "x-generate": True, "x-max-tokens": 12}, | |
| "tags": {"type": "array", "x-generate": True, "x-max-tokens": 5, | |
| "items": {"type": "string"}}}}) | |
| by = {f.pointer: f for f in ok.fields} | |
| assert by["/s"].level_labels == ("low", None, "high") | |
| assert by["/g"].max_new_tokens == 12 and by["/tags/*"].max_new_tokens == 5 | |
| bad = [ | |
| {"type": "string", "x-max-tokens": 4}, # not x-generate | |
| {"type": "string", "x-generate": True, "x-max-tokens": 0}, | |
| {"type": "string", "x-levels": {"1": "a"}}, # not a score field | |
| {"type": "integer", "minimum": 0, "maximum": 2, "x-levels": {"5": "a"}}, | |
| {"type": "integer", "minimum": 0, "maximum": 2, "x-levels": {"1": ""}}, | |
| {"type": "string", "x-other": 1}, | |
| ] | |
| for sub in bad: | |
| with pytest.raises(SchemaError): | |
| compile_schema({"type": "object", "properties": {"f": sub}}) | |