From 8a8d7183991c4c1b14124f3ce78befd4086c96f7 Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Tue, 28 May 2024 13:40:56 -0700 Subject: [PATCH 01/12] Add more info to ParserExceptions --- guidance/_parser.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/guidance/_parser.py b/guidance/_parser.py index cec4a0a54..27c37c8b1 100644 --- a/guidance/_parser.py +++ b/guidance/_parser.py @@ -8,6 +8,7 @@ class ParserException(Exception): def __init__(self, *args, **kwargs): self.current_byte = kwargs.pop("current_byte", None) self.allowed_bytes = kwargs.pop("allowed_bytes", None) + self.consumed_bytes = kwargs.pop("consumed_bytes", None) super().__init__(*args, **kwargs) @@ -357,6 +358,8 @@ def consume_byte(self, byte, log_prob=0.0): raise ParserException( "Attempted to consume a byte that the grammar does not accept!", current_byte=byte, + allowed_bytes=self.valid_next_bytes(), + consumed_bytes=self.bytes, ) if found_invalid: # only update if we changed the set self.state_sets[self.state_set_pos + 1] = OrderedSet(new_next_state_set) From 70c6a2b395c8341383214f75e18670c609c58dab Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Tue, 28 May 2024 13:41:56 -0700 Subject: [PATCH 02/12] black _parser.py --- guidance/_parser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/guidance/_parser.py b/guidance/_parser.py index 27c37c8b1..45a920530 100644 --- a/guidance/_parser.py +++ b/guidance/_parser.py @@ -657,9 +657,9 @@ def _compute_children(self, state_set_pos, item, reversed_state_sets, values_pos if self._compute_children( state_set_pos, item, reversed_state_sets, values_pos + 1 ): - item.children[values_pos] = ( - EarleyItem(value, tuple(), 0, state_set_pos, 0, state_set_pos) # this child has zero length since it was nullable - ) + item.children[values_pos] = EarleyItem( + value, tuple(), 0, state_set_pos, 0, state_set_pos + ) # this child has zero length since it was nullable return True return False From 869b0bccf2f7ded61059a644de1ac1de1ab91d5f Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Tue, 28 May 2024 13:42:53 -0700 Subject: [PATCH 03/12] Use extra parser info to improve json tests --- tests/library/test_json.py | 342 ++++++++++++++++++++++++++----------- 1 file changed, 246 insertions(+), 96 deletions(-) diff --git a/tests/library/test_json.py b/tests/library/test_json.py index 3c127a864..fb3ed272c 100644 --- a/tests/library/test_json.py +++ b/tests/library/test_json.py @@ -1,11 +1,12 @@ import json -from typing import Any, Union +from typing import Any, Union, Set import pytest from jsonschema import validate from guidance import json as gen_json from guidance import models +from guidance._grammar import Byte, ByteRange from guidance._parser import ParserException from guidance.library._json import _to_compact_json @@ -26,7 +27,7 @@ def _generate_and_check( # So append a 'stop' character which we don't # use in any of our tests - STOP_CHAR = "\g" + STOP_CHAR = r"\g" prepared_json = _to_compact_json(target_obj) assert STOP_CHAR not in prepared_json, "STOP_CHAR in string" @@ -64,11 +65,23 @@ def _generate_and_check( ) -def _check_match_failure(bad_string, failure_byte, schema_obj): +def _check_match_failure( + bad_string: str, + good_bytes: bytes, + failure_byte: bytes, + allowed_bytes: Set[Union[Byte, ByteRange]], + schema_obj: dict[str, Any], +): grammar = gen_json(schema=schema_obj) with pytest.raises(ParserException) as pe: grammar.match(bad_string, raise_exceptions=True) + assert pe.value.consumed_bytes[:-1] == good_bytes assert pe.value.current_byte == failure_byte + assert pe.value.allowed_bytes == allowed_bytes + +# Common sets of allowed_bytes +INTEGER_LEADING = {Byte(b"-"), Byte(b"0"), ByteRange(b"19")} +INTEGER_FOLLOWING = {ByteRange(b"09")} def test_null(): @@ -111,19 +124,25 @@ def test_integer_schema(self, my_int): _generate_and_check(my_int, schema_obj) @pytest.mark.parametrize( - ["bad_string", "failure_byte"], + ["bad_string", "good_bytes", "failure_byte", "allowed_bytes"], [ - ("9999a7777", b"a"), - ("123, []", b","), - ("a321", b"a"), - ("123789.456", b"."), - ("[]", b"["), - ('{"a":4}', b"{"), + ("9999a7777", b"9999", b"a", INTEGER_FOLLOWING), + ("123, []", b"123", b",", INTEGER_FOLLOWING), + ("a321", b"", b"a", INTEGER_LEADING), + ("123789.456", b"123789", b".", INTEGER_FOLLOWING), + ("[]", b"", b"[", INTEGER_LEADING), + ('{"a":4}', b"", b"{", INTEGER_LEADING), ], ) - def test_bad_integer(self, bad_string, failure_byte): + def test_bad_integer(self, bad_string, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(TestInteger.schema) - _check_match_failure(bad_string, failure_byte, schema_obj) + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) class TestNumber: @@ -160,19 +179,24 @@ def test_number(self, target_obj, temperature): _generate_and_check(target_obj, schema_obj, desired_temperature=temperature) @pytest.mark.parametrize( - ["bad_string", "failure_byte"], + ["bad_string", "good_bytes", "failure_byte", "allowed_bytes"], [ - ("9999a7777", b"a"), - ("123.6, []", b","), - ("a321", b"a"), - ("[]", b"["), - ('{"a":4}', b"{"), + ("9999a7777", b"9999", b"a", {Byte(b"e"), Byte(b"."), *INTEGER_FOLLOWING}), + ("123.6, []", b"123.6", b",", {Byte(b"e"), *INTEGER_FOLLOWING}), + ("a321", b"", b"a", INTEGER_LEADING), + ("[]", b"", b"[", INTEGER_LEADING), + ('{"a":4}', b"", b"{", INTEGER_LEADING), ], ) - def test_bad_number(self, bad_string, failure_byte): + def test_bad_number(self, bad_string, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(TestNumber.schema) - _check_match_failure(bad_string, failure_byte, schema_obj) - + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) @pytest.mark.parametrize( "my_string", @@ -308,14 +332,14 @@ def test_object_containing_list(self, temperature): _generate_and_check(target_obj, schema_obj, desired_temperature=temperature) @pytest.mark.parametrize( - ["bad_string", "failure_byte"], + ["bad_string", "good_bytes", "failure_byte", "allowed_bytes"], [ - ("9999a7777", b"9"), - ('{"a":1255.4567}', b"."), - ('{"a":"123"}', b'"'), + ("9999a7777", b"", b"9", {Byte(b"{")}), + ('{"a":1255.4567}', b'{"a":1255', b".", {Byte(b"}"), *INTEGER_FOLLOWING}), + ('{"a":"123"}', b'{"a":', b'"', INTEGER_LEADING), ], ) - def test_bad_object(self, bad_string, failure_byte): + def test_bad_object(self, bad_string, good_bytes, failure_byte, allowed_bytes): schema = """{ "type": "object", "properties": { @@ -324,8 +348,13 @@ def test_bad_object(self, bad_string, failure_byte): } """ schema_obj = json.loads(schema) - _check_match_failure(bad_string, failure_byte, schema_obj) - + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) class TestSimpleArray: # These are array without references @@ -391,14 +420,14 @@ def test_object_list(self, target_obj, temperature): _generate_and_check(target_obj, schema_obj, desired_temperature=temperature) @pytest.mark.parametrize( - ["bad_string", "failure_byte"], + ["bad_string", "good_bytes", "failure_byte", "allowed_bytes"], [ - ("9999a7777", b"9"), - ("[321.654]", b"."), - ('["123"]', b'"'), + ("9999a7777", b"", b"9", {Byte(b"[")}), + ("[321.654]", b"[321", b".", {Byte(b"]"), Byte(b','), *INTEGER_FOLLOWING}), + ('["123"]', b"[", b'"', {Byte(b']'), *INTEGER_LEADING}), ], ) - def test_bad_object(self, bad_string, failure_byte): + def test_bad_object(self, bad_string, good_bytes, failure_byte, allowed_bytes): schema = """{ "type" : "array", "items" : { @@ -406,8 +435,13 @@ def test_bad_object(self, bad_string, failure_byte): } }""" schema_obj = json.loads(schema) - _check_match_failure(bad_string, failure_byte, schema_obj) - + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) class TestArrayWithLengthConstraints: prefix_schema_obj = [{"type": "integer"}, {"type": "boolean"}] @@ -494,44 +528,68 @@ def test_good_with_items(self, min_items, max_items, target_obj): _generate_and_check(target_obj, schema_obj) @pytest.mark.parametrize( - "min_items, max_items, bad_obj, failure_byte", + "min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes", [ ( 1, 4, [42, "string_not_bool", "hello", "extra"], + b"[42,", b'"', + {Byte(b"t"), Byte(b"f")}, ), # Second item does not match prefix schema ( 0, 3, [42, True, 100], + b"[42,true,", b"1", + {Byte(b'"')}, ), # Last item does not match general item schema ( 3, 5, [42, True, "valid", "extra1", "extra2", "too_many"], + b'[42,true,"valid","extra1","extra2"', b",", + {Byte(b"]")}, ), # Exceeds maxItems - (2, 3, [42], b"]"), # Not enough items - (1, 1, [42, True], b","), # Too many items for maxItems + ( + 2, + 3, + [42], + b"[42", + b"]", + {Byte(b","), *INTEGER_FOLLOWING}, + ), # Not enough items + ( + 1, + 1, + [42, True], + b"[42", + b",", + {Byte(b"]"), *INTEGER_FOLLOWING}, + ), # Too many items for maxItems ( 0, 0, [42, True, "str"], + b"[", b"4", + {Byte(b"]")}, ), # maxItems set to 0, but array is not empty ( 3, 5, [42, True], + b"[42,true", b"]", + {Byte(b",")}, ), # Array has one fewer item than required by minItems ], ) def test_bad_with_prefix_and_items( - self, min_items, max_items, bad_obj, failure_byte + self, min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes ): schema_obj = { "prefixItems": self.prefix_schema_obj, @@ -541,39 +599,59 @@ def test_bad_with_prefix_and_items( "type": "array", } bad_string = _to_compact_json(bad_obj) - _check_match_failure(bad_string, failure_byte, schema_obj) - + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) @pytest.mark.parametrize( - "min_items, max_items, bad_obj, failure_byte", + "min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes", [ ( 2, 2, [42], + b"[42", b"]", + {Byte(b","), *INTEGER_FOLLOWING}, ), # Array too short to meet minItems, despite matching prefixItems ( 1, 2, [42, "not_bool"], + b"[42,", b'"', + {Byte(b"t"), Byte(b"f")}, ), # Second item violates prefixItems type requirement ( 0, 1, [42, True], + b"[42", b",", + {Byte(b"]"), *INTEGER_FOLLOWING}, ), # Array exceeds maxItems with valid prefixItems types ( 1, 5, [42, True, "extra"], + b"[42,true", b",", + {Byte(b"]")}, ), # Item beyond prefixItems with no "items" schema - (0, 0, [42], b"4"), # maxItems set to 0, but array is not empty + ( + 0, + 0, + [42], + b"[", + b"4", + {Byte(b"]")}, + ), # maxItems set to 0, but array is not empty ], ) - def test_bad_with_prefix(self, min_items, max_items, bad_obj, failure_byte): + def test_bad_with_prefix(self, min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = { "prefixItems": self.prefix_schema_obj, "minItems": min_items, @@ -581,18 +659,51 @@ def test_bad_with_prefix(self, min_items, max_items, bad_obj, failure_byte): "type": "array", } bad_string = _to_compact_json(bad_obj) - _check_match_failure(bad_string, failure_byte, schema_obj) - + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) @pytest.mark.parametrize( - "min_items, max_items, bad_obj, failure_byte", + "min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes", [ - (1, 2, ["hello", "world", "extra"], b","), # Too many items for maxItems - (2, 3, ["hello"], b"]"), # Not enough items - (2, 3, ["hello", 42], b"4"), # Badly typed second item - (0, 0, ["hello"], b'"'), # maxItems set to 0, but array is not empty + ( + 1, + 2, + ["hello", "world", "extra"], + b'["hello","world"', + b",", + {Byte(b"]")}, + ), # Too many items for maxItems + ( + 2, + 3, + ["hello"], + b'["hello"', + b"]", + {Byte(b",")}, + ), # Not enough items + ( + 2, + 3, + ["hello", 42], + b'["hello",', + b"4", + {Byte(b'"')}, + ), # Badly typed second item + ( + 0, + 0, + ["hello"], + b"[", + b'"', + {Byte(b"]")}, + ), # maxItems set to 0, but array is not empty ], ) - def test_bad_with_items(self, min_items, max_items, bad_obj, failure_byte): + def test_bad_with_items(self, min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = { "items": self.items_schema_obj, "minItems": min_items, @@ -600,8 +711,13 @@ def test_bad_with_items(self, min_items, max_items, bad_obj, failure_byte): "type": "array", } bad_string = _to_compact_json(bad_obj) - _check_match_failure(bad_string, failure_byte, schema_obj) - + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) class TestWithReferences: @pytest.mark.parametrize( @@ -936,31 +1052,41 @@ def test_enum(self, target_obj, temperature): _generate_and_check(target_obj, schema_obj, desired_temperature=temperature) @pytest.mark.parametrize( - "bad_obj, failure_byte", + "bad_obj, good_bytes, failure_byte, allowed_bytes", [ - ("1", b"1"), - (2, b"2"), - (True, b"t"), + ("1", b'"', b"1", {Byte(b"2")}), + (2, b"", b"2", {Byte(b'"'), Byte(b"1"), Byte(b"f")}), + (True, b"", b"t", {Byte(b'"'), Byte(b"1"), Byte(b"f")}), ], ) - def test_bad_enum(self, bad_obj, failure_byte): + def test_bad_enum(self, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(self.simple_schema) - bad_str = _to_compact_json(bad_obj) - _check_match_failure(bad_str, failure_byte, schema_obj) - + bad_string = _to_compact_json(bad_obj) + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) @pytest.mark.parametrize( - "bad_obj, failure_byte", + "bad_obj, good_bytes, failure_byte, allowed_bytes", [ - ("ab", b"b"), - ("bc", b"c"), - ("ca", b"a"), + ("ab", b'"a', b"b", {Byte(b"a")}), + ("bc", b'"b', b"c", {Byte(b"b")}), + ("ca", b'"c', b"a", {Byte(b"c")}), ], ) - def test_bad_prefix_enum(self, bad_obj, failure_byte): + def test_bad_prefix_enum(self, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(self.prefix_schema) - bad_str = _to_compact_json(bad_obj) - _check_match_failure(bad_str, failure_byte, schema_obj) - + bad_string = _to_compact_json(bad_obj) + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) class TestAdditionalProperties: @@ -976,7 +1102,7 @@ class TestAdditionalProperties: "type": "object", "additionalProperties": { "anyOf": [ - {"type" : "string"}, + {"type": "string"}, {"type": "integer"} ] } @@ -1005,17 +1131,22 @@ def test_simple_additional_properties(self, target_obj, temperature): _generate_and_check(target_obj, schema_obj, desired_temperature=temperature) @pytest.mark.parametrize( - "bad_obj, failure_byte", + "bad_obj, good_bytes, failure_byte, allowed_bytes", [ - ({"a": "1"}, b'"'), - ({"a": 1, "b": 1.5}, b"."), + ({"a": "1"}, b'{"a":', b'"', INTEGER_LEADING), + ({"a": 1, "b": 1.5}, b'{"a":1,"b":1', b".", {Byte(b","), Byte(b"}"), *INTEGER_FOLLOWING}), ], ) - def test_simple_bad_type(self, bad_obj, failure_byte): + def test_simple_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(self.simple_schema) bad_string = _to_compact_json(bad_obj) - _check_match_failure(bad_string, failure_byte, schema_obj) - + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) @pytest.mark.parametrize( "target_obj", [{}, {"a": 1}, {"a": "2"}, {"a": 1, "b": "2"}] ) @@ -1028,14 +1159,23 @@ def test_anyOf_additional_properties(self, target_obj): _generate_and_check(target_obj, schema_obj) @pytest.mark.parametrize( - "bad_obj, failure_byte", - [({"a": 1.5}, b"."), ({"a": True}, b"t"), ({"a": 1, "b": False}, b"f")], + "bad_obj, good_bytes, failure_byte, allowed_bytes", + [ + ({"a": 1.5}, b'{"a":1', b".", {Byte(b","), Byte(b"}"), *INTEGER_FOLLOWING}), + ({"a": True}, b'{"a":', b"t", {Byte(b'"'), *INTEGER_LEADING}), + ({"a": 1, "b": False}, b'{"a":1,"b":', b"f", {Byte(b'"'), *INTEGER_LEADING}), + ], ) - def test_anyOf_bad_type(self, bad_obj, failure_byte): + def test_anyOf_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(self.anyOf_schema) bad_string = _to_compact_json(bad_obj) - _check_match_failure(bad_string, failure_byte, schema_obj) - + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) @pytest.mark.parametrize( "target_obj", [ @@ -1054,31 +1194,41 @@ def test_properties_and_additional_properties(self, target_obj, temperature): _generate_and_check(target_obj, schema_obj, desired_temperature=temperature) @pytest.mark.parametrize( - "bad_obj, failure_byte", + "bad_obj, good_bytes, failure_byte, allowed_bytes", [ - ({}, b"}"), - ({"a": 1}, b"a"), - ({"a": 1, "b": 2}, b"a"), + ({}, b"{", b"}", {Byte(b'"')}), + ({"a": 1}, b'{"', b"a", {Byte(b"m")}), + ({"a": 1, "b": 2}, b'{"', b"a", {Byte(b"m")}), ], ) - def test_combined_missing_properties(self, bad_obj, failure_byte): + def test_combined_missing_properties(self, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(self.combined_schema) bad_string = _to_compact_json(bad_obj) - _check_match_failure(bad_string, failure_byte, schema_obj) - + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) @pytest.mark.parametrize( - "bad_obj, failure_byte", + "bad_obj, good_bytes, failure_byte, allowed_bytes", [ - ({"mystr": 1}, b"1"), - ({"mystr": 1, "a": 2}, b"1"), - ({"mystr": "hello", "a": False}, b"f"), + ({"mystr": 1}, b'{"mystr":', b"1", {Byte(b'"')}), + ({"mystr": 1, "a": 2}, b'{"mystr":', b"1", {Byte(b'"')}), + ({"mystr": "hello", "a": False}, b'{"mystr":"hello","a":', b"f", INTEGER_LEADING), ], ) - def test_combined_bad_type(self, bad_obj, failure_byte): + def test_combined_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(self.combined_schema) bad_string = _to_compact_json(bad_obj) - _check_match_failure(bad_string, failure_byte, schema_obj) - + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + schema_obj=schema_obj, + ) class TestRecursiveStructures: @pytest.mark.parametrize( From fcec162fafe30368236918f73c1c0d3fa44198be Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Tue, 28 May 2024 13:44:03 -0700 Subject: [PATCH 04/12] black test_json.py --- tests/library/test_json.py | 50 ++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/tests/library/test_json.py b/tests/library/test_json.py index fb3ed272c..cf5c98a43 100644 --- a/tests/library/test_json.py +++ b/tests/library/test_json.py @@ -79,6 +79,7 @@ def _check_match_failure( assert pe.value.current_byte == failure_byte assert pe.value.allowed_bytes == allowed_bytes + # Common sets of allowed_bytes INTEGER_LEADING = {Byte(b"-"), Byte(b"0"), ByteRange(b"19")} INTEGER_FOLLOWING = {ByteRange(b"09")} @@ -198,6 +199,7 @@ def test_bad_number(self, bad_string, good_bytes, failure_byte, allowed_bytes): schema_obj=schema_obj, ) + @pytest.mark.parametrize( "my_string", [ @@ -356,6 +358,7 @@ def test_bad_object(self, bad_string, good_bytes, failure_byte, allowed_bytes): schema_obj=schema_obj, ) + class TestSimpleArray: # These are array without references @pytest.mark.parametrize("target_obj", [[], [0], [34, 56], [1, 2, 3], [9, 8, 7, 6]]) @@ -423,8 +426,8 @@ def test_object_list(self, target_obj, temperature): ["bad_string", "good_bytes", "failure_byte", "allowed_bytes"], [ ("9999a7777", b"", b"9", {Byte(b"[")}), - ("[321.654]", b"[321", b".", {Byte(b"]"), Byte(b','), *INTEGER_FOLLOWING}), - ('["123"]', b"[", b'"', {Byte(b']'), *INTEGER_LEADING}), + ("[321.654]", b"[321", b".", {Byte(b"]"), Byte(b","), *INTEGER_FOLLOWING}), + ('["123"]', b"[", b'"', {Byte(b"]"), *INTEGER_LEADING}), ], ) def test_bad_object(self, bad_string, good_bytes, failure_byte, allowed_bytes): @@ -443,6 +446,7 @@ def test_bad_object(self, bad_string, good_bytes, failure_byte, allowed_bytes): schema_obj=schema_obj, ) + class TestArrayWithLengthConstraints: prefix_schema_obj = [{"type": "integer"}, {"type": "boolean"}] items_schema_obj = {"type": "string"} @@ -606,6 +610,7 @@ def test_bad_with_prefix_and_items( allowed_bytes=allowed_bytes, schema_obj=schema_obj, ) + @pytest.mark.parametrize( "min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes", [ @@ -651,7 +656,9 @@ def test_bad_with_prefix_and_items( ), # maxItems set to 0, but array is not empty ], ) - def test_bad_with_prefix(self, min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes): + def test_bad_with_prefix( + self, min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes + ): schema_obj = { "prefixItems": self.prefix_schema_obj, "minItems": min_items, @@ -666,6 +673,7 @@ def test_bad_with_prefix(self, min_items, max_items, bad_obj, good_bytes, failur allowed_bytes=allowed_bytes, schema_obj=schema_obj, ) + @pytest.mark.parametrize( "min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes", [ @@ -703,7 +711,9 @@ def test_bad_with_prefix(self, min_items, max_items, bad_obj, good_bytes, failur ), # maxItems set to 0, but array is not empty ], ) - def test_bad_with_items(self, min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes): + def test_bad_with_items( + self, min_items, max_items, bad_obj, good_bytes, failure_byte, allowed_bytes + ): schema_obj = { "items": self.items_schema_obj, "minItems": min_items, @@ -719,6 +729,7 @@ def test_bad_with_items(self, min_items, max_items, bad_obj, good_bytes, failure schema_obj=schema_obj, ) + class TestWithReferences: @pytest.mark.parametrize( "target_obj", @@ -1069,6 +1080,7 @@ def test_bad_enum(self, bad_obj, good_bytes, failure_byte, allowed_bytes): allowed_bytes=allowed_bytes, schema_obj=schema_obj, ) + @pytest.mark.parametrize( "bad_obj, good_bytes, failure_byte, allowed_bytes", [ @@ -1088,6 +1100,7 @@ def test_bad_prefix_enum(self, bad_obj, good_bytes, failure_byte, allowed_bytes) schema_obj=schema_obj, ) + class TestAdditionalProperties: simple_schema = """{ @@ -1134,7 +1147,12 @@ def test_simple_additional_properties(self, target_obj, temperature): "bad_obj, good_bytes, failure_byte, allowed_bytes", [ ({"a": "1"}, b'{"a":', b'"', INTEGER_LEADING), - ({"a": 1, "b": 1.5}, b'{"a":1,"b":1', b".", {Byte(b","), Byte(b"}"), *INTEGER_FOLLOWING}), + ( + {"a": 1, "b": 1.5}, + b'{"a":1,"b":1', + b".", + {Byte(b","), Byte(b"}"), *INTEGER_FOLLOWING}, + ), ], ) def test_simple_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_bytes): @@ -1147,6 +1165,7 @@ def test_simple_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_bytes) allowed_bytes=allowed_bytes, schema_obj=schema_obj, ) + @pytest.mark.parametrize( "target_obj", [{}, {"a": 1}, {"a": "2"}, {"a": 1, "b": "2"}] ) @@ -1163,7 +1182,12 @@ def test_anyOf_additional_properties(self, target_obj): [ ({"a": 1.5}, b'{"a":1', b".", {Byte(b","), Byte(b"}"), *INTEGER_FOLLOWING}), ({"a": True}, b'{"a":', b"t", {Byte(b'"'), *INTEGER_LEADING}), - ({"a": 1, "b": False}, b'{"a":1,"b":', b"f", {Byte(b'"'), *INTEGER_LEADING}), + ( + {"a": 1, "b": False}, + b'{"a":1,"b":', + b"f", + {Byte(b'"'), *INTEGER_LEADING}, + ), ], ) def test_anyOf_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_bytes): @@ -1176,6 +1200,7 @@ def test_anyOf_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_bytes): allowed_bytes=allowed_bytes, schema_obj=schema_obj, ) + @pytest.mark.parametrize( "target_obj", [ @@ -1201,7 +1226,9 @@ def test_properties_and_additional_properties(self, target_obj, temperature): ({"a": 1, "b": 2}, b'{"', b"a", {Byte(b"m")}), ], ) - def test_combined_missing_properties(self, bad_obj, good_bytes, failure_byte, allowed_bytes): + def test_combined_missing_properties( + self, bad_obj, good_bytes, failure_byte, allowed_bytes + ): schema_obj = json.loads(self.combined_schema) bad_string = _to_compact_json(bad_obj) _check_match_failure( @@ -1211,12 +1238,18 @@ def test_combined_missing_properties(self, bad_obj, good_bytes, failure_byte, al allowed_bytes=allowed_bytes, schema_obj=schema_obj, ) + @pytest.mark.parametrize( "bad_obj, good_bytes, failure_byte, allowed_bytes", [ ({"mystr": 1}, b'{"mystr":', b"1", {Byte(b'"')}), ({"mystr": 1, "a": 2}, b'{"mystr":', b"1", {Byte(b'"')}), - ({"mystr": "hello", "a": False}, b'{"mystr":"hello","a":', b"f", INTEGER_LEADING), + ( + {"mystr": "hello", "a": False}, + b'{"mystr":"hello","a":', + b"f", + INTEGER_LEADING, + ), ], ) def test_combined_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_bytes): @@ -1230,6 +1263,7 @@ def test_combined_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_byte schema_obj=schema_obj, ) + class TestRecursiveStructures: @pytest.mark.parametrize( "target_obj", From 90c1029f9521f9952447bea0f823033f344a5fab Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Tue, 28 May 2024 13:44:37 -0700 Subject: [PATCH 05/12] Use extra parser info to improve pydantic tests --- tests/library/test_pydantic.py | 36 ++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/tests/library/test_pydantic.py b/tests/library/test_pydantic.py index 447285db3..99d51a858 100644 --- a/tests/library/test_pydantic.py +++ b/tests/library/test_pydantic.py @@ -1,6 +1,6 @@ import inspect from json import dumps as json_dumps -from typing import Any, Dict, Generic, List, Literal, Tuple, Type, TypeVar, Union +from typing import Any, Dict, Generic, List, Literal, Tuple, Type, TypeVar, Union, Set import pydantic import pytest @@ -8,6 +8,7 @@ from guidance import json as gen_json from guidance import models +from guidance._grammar import Byte, ByteRange from guidance._parser import ParserException @@ -84,18 +85,20 @@ def generate_and_check( round_trip_object = validate_string(lm[CAPTURE_KEY], pydantic_model) assert round_trip_object == target_obj - def check_match_failure( bad_obj: Any, + good_bytes: bytes, failure_byte: bytes, + allowed_bytes: Set[Union[Byte, ByteRange]], pydantic_model: Union[Type[pydantic.BaseModel], pydantic.TypeAdapter], ): bad_string = to_compact_json(bad_obj) grammar = gen_json(schema=pydantic_model) with pytest.raises(ParserException) as pe: grammar.match(bad_string, raise_exceptions=True) + assert pe.value.consumed_bytes[:-1] == good_bytes assert pe.value.current_byte == failure_byte - + assert pe.value.allowed_bytes == allowed_bytes def test_simple_model(): class Simple(pydantic.BaseModel): @@ -169,7 +172,13 @@ def test_heterogeneous(self): def test_maxitems(self): model = pydantic.TypeAdapter(Tuple[int,]) - check_match_failure((1, 2), b",", model) + check_match_failure( + bad_obj=(1, 2), + good_bytes=b"[1", + failure_byte=b",", + allowed_bytes={ByteRange(b"09"), Byte(b"]")}, + pydantic_model=model + ) class TestDict: @@ -239,14 +248,21 @@ def test_generic(self, my_type, my_obj): generate_and_check(obj, model) @pytest.mark.parametrize( - "my_type, my_obj, failure_byte", + "my_type, my_obj, good_bytes, failure_byte, allowed_bytes", [ - (bool, "True", b'"'), - (str, 42, b"4"), - (int, False, b"f"), + (bool, "True", b'', b'"', {Byte(b"t"), Byte(b"f")}), + (str, 42, b'', b"4", {Byte(b'"')}), + (int, False, b'', b"f", {Byte(b'0'), ByteRange(b'19'), Byte(b'-')}), ], ) - def test_bad_generic(self, my_type, my_obj, failure_byte): + def test_bad_generic(self, my_type, my_obj, good_bytes, failure_byte, allowed_bytes): model = self.SimpleGeneric[my_type] obj = {"my_obj": my_obj} - check_match_failure(obj, failure_byte, model) + check_match_failure( + bad_obj=obj, + good_bytes=b'{"my_obj":' + good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + pydantic_model=model + ) + From 51337dce2c003e1f9e905e34dbc022d5b826eecb Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Tue, 28 May 2024 13:45:13 -0700 Subject: [PATCH 06/12] black test_pydantic.py --- tests/library/test_pydantic.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/library/test_pydantic.py b/tests/library/test_pydantic.py index 99d51a858..6ecdc1879 100644 --- a/tests/library/test_pydantic.py +++ b/tests/library/test_pydantic.py @@ -85,6 +85,7 @@ def generate_and_check( round_trip_object = validate_string(lm[CAPTURE_KEY], pydantic_model) assert round_trip_object == target_obj + def check_match_failure( bad_obj: Any, good_bytes: bytes, @@ -100,6 +101,7 @@ def check_match_failure( assert pe.value.current_byte == failure_byte assert pe.value.allowed_bytes == allowed_bytes + def test_simple_model(): class Simple(pydantic.BaseModel): my_string: str @@ -176,8 +178,8 @@ def test_maxitems(self): bad_obj=(1, 2), good_bytes=b"[1", failure_byte=b",", - allowed_bytes={ByteRange(b"09"), Byte(b"]")}, - pydantic_model=model + allowed_bytes={ByteRange(b"09"), Byte(b"]")}, + pydantic_model=model, ) @@ -250,12 +252,14 @@ def test_generic(self, my_type, my_obj): @pytest.mark.parametrize( "my_type, my_obj, good_bytes, failure_byte, allowed_bytes", [ - (bool, "True", b'', b'"', {Byte(b"t"), Byte(b"f")}), - (str, 42, b'', b"4", {Byte(b'"')}), - (int, False, b'', b"f", {Byte(b'0'), ByteRange(b'19'), Byte(b'-')}), + (bool, "True", b"", b'"', {Byte(b"t"), Byte(b"f")}), + (str, 42, b"", b"4", {Byte(b'"')}), + (int, False, b"", b"f", {Byte(b"0"), ByteRange(b"19"), Byte(b"-")}), ], ) - def test_bad_generic(self, my_type, my_obj, good_bytes, failure_byte, allowed_bytes): + def test_bad_generic( + self, my_type, my_obj, good_bytes, failure_byte, allowed_bytes + ): model = self.SimpleGeneric[my_type] obj = {"my_obj": my_obj} check_match_failure( @@ -263,6 +267,5 @@ def test_bad_generic(self, my_type, my_obj, good_bytes, failure_byte, allowed_by good_bytes=b'{"my_obj":' + good_bytes, failure_byte=failure_byte, allowed_bytes=allowed_bytes, - pydantic_model=model + pydantic_model=model, ) - From e67075266d3707bc6e0094bcf0e1019b34cf1946 Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Tue, 28 May 2024 13:59:54 -0700 Subject: [PATCH 07/12] Move check_match_failure to common test utils --- tests/library/test_json.py | 42 ++++++++++++++++++---------------- tests/library/test_pydantic.py | 14 +++++++----- tests/utils.py | 23 ++++++++++++++++++- 3 files changed, 52 insertions(+), 27 deletions(-) diff --git a/tests/library/test_json.py b/tests/library/test_json.py index cf5c98a43..5ae893bb0 100644 --- a/tests/library/test_json.py +++ b/tests/library/test_json.py @@ -7,8 +7,8 @@ from guidance import json as gen_json from guidance import models from guidance._grammar import Byte, ByteRange -from guidance._parser import ParserException from guidance.library._json import _to_compact_json +from ..utils import check_match_failure as _check_match_failure def _generate_and_check( @@ -65,7 +65,7 @@ def _generate_and_check( ) -def _check_match_failure( +def check_match_failure( bad_string: str, good_bytes: bytes, failure_byte: bytes, @@ -73,11 +73,13 @@ def _check_match_failure( schema_obj: dict[str, Any], ): grammar = gen_json(schema=schema_obj) - with pytest.raises(ParserException) as pe: - grammar.match(bad_string, raise_exceptions=True) - assert pe.value.consumed_bytes[:-1] == good_bytes - assert pe.value.current_byte == failure_byte - assert pe.value.allowed_bytes == allowed_bytes + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + grammar=grammar, + ) # Common sets of allowed_bytes @@ -137,7 +139,7 @@ def test_integer_schema(self, my_int): ) def test_bad_integer(self, bad_string, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(TestInteger.schema) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -191,7 +193,7 @@ def test_number(self, target_obj, temperature): ) def test_bad_number(self, bad_string, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(TestNumber.schema) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -350,7 +352,7 @@ def test_bad_object(self, bad_string, good_bytes, failure_byte, allowed_bytes): } """ schema_obj = json.loads(schema) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -438,7 +440,7 @@ def test_bad_object(self, bad_string, good_bytes, failure_byte, allowed_bytes): } }""" schema_obj = json.loads(schema) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -603,7 +605,7 @@ def test_bad_with_prefix_and_items( "type": "array", } bad_string = _to_compact_json(bad_obj) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -666,7 +668,7 @@ def test_bad_with_prefix( "type": "array", } bad_string = _to_compact_json(bad_obj) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -721,7 +723,7 @@ def test_bad_with_items( "type": "array", } bad_string = _to_compact_json(bad_obj) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -1073,7 +1075,7 @@ def test_enum(self, target_obj, temperature): def test_bad_enum(self, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(self.simple_schema) bad_string = _to_compact_json(bad_obj) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -1092,7 +1094,7 @@ def test_bad_enum(self, bad_obj, good_bytes, failure_byte, allowed_bytes): def test_bad_prefix_enum(self, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(self.prefix_schema) bad_string = _to_compact_json(bad_obj) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -1158,7 +1160,7 @@ def test_simple_additional_properties(self, target_obj, temperature): def test_simple_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(self.simple_schema) bad_string = _to_compact_json(bad_obj) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -1193,7 +1195,7 @@ def test_anyOf_additional_properties(self, target_obj): def test_anyOf_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(self.anyOf_schema) bad_string = _to_compact_json(bad_obj) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -1231,7 +1233,7 @@ def test_combined_missing_properties( ): schema_obj = json.loads(self.combined_schema) bad_string = _to_compact_json(bad_obj) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, @@ -1255,7 +1257,7 @@ def test_combined_missing_properties( def test_combined_bad_type(self, bad_obj, good_bytes, failure_byte, allowed_bytes): schema_obj = json.loads(self.combined_schema) bad_string = _to_compact_json(bad_obj) - _check_match_failure( + check_match_failure( bad_string=bad_string, good_bytes=good_bytes, failure_byte=failure_byte, diff --git a/tests/library/test_pydantic.py b/tests/library/test_pydantic.py index 6ecdc1879..016d9a4e5 100644 --- a/tests/library/test_pydantic.py +++ b/tests/library/test_pydantic.py @@ -9,7 +9,7 @@ from guidance import json as gen_json from guidance import models from guidance._grammar import Byte, ByteRange -from guidance._parser import ParserException +from ..utils import check_match_failure as _check_match_failure def to_compact_json(target: Any) -> str: @@ -95,11 +95,13 @@ def check_match_failure( ): bad_string = to_compact_json(bad_obj) grammar = gen_json(schema=pydantic_model) - with pytest.raises(ParserException) as pe: - grammar.match(bad_string, raise_exceptions=True) - assert pe.value.consumed_bytes[:-1] == good_bytes - assert pe.value.current_byte == failure_byte - assert pe.value.allowed_bytes == allowed_bytes + _check_match_failure( + bad_string=bad_string, + good_bytes=good_bytes, + failure_byte=failure_byte, + allowed_bytes=allowed_bytes, + grammar=grammar, + ) def test_simple_model(): diff --git a/tests/utils.py b/tests/utils.py index 0e7982e2c..cb38d62dd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,10 +1,12 @@ import os -from typing import Any +from typing import Set, Union import pytest from huggingface_hub import hf_hub_download import guidance +from guidance._grammar import Byte, ByteRange, GrammarFunction +from guidance._parser import ParserException opanai_model_cache = {} @@ -132,3 +134,22 @@ def get_azure_guidance_model(model_name, caching=False, **kwargs): ) return azure_guidance_model_cache[key] + + +def check_match_failure( + bad_string: str, + good_bytes: bytes, + failure_byte: bytes, + allowed_bytes: Set[Union[Byte, ByteRange]], + grammar: GrammarFunction, +): + """ + Helper function to check that a string fails to match a grammar after consuming + zero or more bytes. It checks that the consumed bytes are as expected, that the + failure byte is as expected, and that the allowed bytes are as expected. + """ + with pytest.raises(ParserException) as pe: + grammar.match(bad_string, raise_exceptions=True) + assert pe.value.consumed_bytes[:-1] == good_bytes + assert pe.value.current_byte == failure_byte + assert pe.value.allowed_bytes == allowed_bytes \ No newline at end of file From 457c72c9327bd6aa03ba5350ce34091bad11d61d Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Tue, 28 May 2024 14:00:36 -0700 Subject: [PATCH 08/12] black utils.py --- tests/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index cb38d62dd..a8df4802f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -10,6 +10,7 @@ opanai_model_cache = {} + def env_or_fail(var_name: str) -> str: env_value = os.getenv(var_name, None) @@ -17,6 +18,7 @@ def env_or_fail(var_name: str) -> str: return env_value + def get_model(model_name, caching=False, **kwargs): """Get an LLM by name.""" if model_name.startswith("openai:"): @@ -97,9 +99,7 @@ def get_llama_cpp_model(model_name, caching=False, **kwargs): # load it over and over again key = model_name + "_" + str(caching) + "_" + str(kwargs) if key not in llama_cpp_model_cache: - llama_cpp_model_cache[key] = guidance.models.LlamaCpp( - model_name, **kwargs - ) + llama_cpp_model_cache[key] = guidance.models.LlamaCpp(model_name, **kwargs) return llama_cpp_model_cache[key] @@ -152,4 +152,4 @@ def check_match_failure( grammar.match(bad_string, raise_exceptions=True) assert pe.value.consumed_bytes[:-1] == good_bytes assert pe.value.current_byte == failure_byte - assert pe.value.allowed_bytes == allowed_bytes \ No newline at end of file + assert pe.value.allowed_bytes == allowed_bytes From 1c8ed95499b1c50c042d6e7a06718bc6c76eeb17 Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Tue, 28 May 2024 16:24:18 -0700 Subject: [PATCH 09/12] move trailing comment --- guidance/_parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/guidance/_parser.py b/guidance/_parser.py index 45a920530..600d0cb43 100644 --- a/guidance/_parser.py +++ b/guidance/_parser.py @@ -657,9 +657,10 @@ def _compute_children(self, state_set_pos, item, reversed_state_sets, values_pos if self._compute_children( state_set_pos, item, reversed_state_sets, values_pos + 1 ): + # this child has zero length since it was nullable item.children[values_pos] = EarleyItem( value, tuple(), 0, state_set_pos, 0, state_set_pos - ) # this child has zero length since it was nullable + ) return True return False From b7a4b62a6851284c1c3df06059e0051d3905d037 Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Tue, 28 May 2024 16:24:42 -0700 Subject: [PATCH 10/12] r"\g" -> "\g" --- tests/library/test_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/library/test_json.py b/tests/library/test_json.py index 5ae893bb0..b96d6d788 100644 --- a/tests/library/test_json.py +++ b/tests/library/test_json.py @@ -27,7 +27,7 @@ def _generate_and_check( # So append a 'stop' character which we don't # use in any of our tests - STOP_CHAR = r"\g" + STOP_CHAR = "\g" prepared_json = _to_compact_json(target_obj) assert STOP_CHAR not in prepared_json, "STOP_CHAR in string" From 5fe0468dee3ff95b7a4468422f380d16fb83d438 Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Thu, 30 May 2024 17:37:58 -0700 Subject: [PATCH 11/12] '/g'->chr(7) --- tests/library/test_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/library/test_json.py b/tests/library/test_json.py index b96d6d788..494b94f6d 100644 --- a/tests/library/test_json.py +++ b/tests/library/test_json.py @@ -27,7 +27,7 @@ def _generate_and_check( # So append a 'stop' character which we don't # use in any of our tests - STOP_CHAR = "\g" + STOP_CHAR = chr(7) prepared_json = _to_compact_json(target_obj) assert STOP_CHAR not in prepared_json, "STOP_CHAR in string" From 027366224aafd4b5c5d75d2ed2d664bdb778ef04 Mon Sep 17 00:00:00 2001 From: Hudson Cooper Date: Fri, 31 May 2024 09:41:28 -0700 Subject: [PATCH 12/12] 3.8 compat --- tests/library/test_json.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/library/test_json.py b/tests/library/test_json.py index 494b94f6d..14a0a6458 100644 --- a/tests/library/test_json.py +++ b/tests/library/test_json.py @@ -1,5 +1,5 @@ import json -from typing import Any, Union, Set +from typing import Any, Union, Set, Dict import pytest from jsonschema import validate @@ -70,7 +70,7 @@ def check_match_failure( good_bytes: bytes, failure_byte: bytes, allowed_bytes: Set[Union[Byte, ByteRange]], - schema_obj: dict[str, Any], + schema_obj: Dict[str, Any], ): grammar = gen_json(schema=schema_obj) _check_match_failure(