From 5980e5b7e5bbde9bb3078afe3003b6a15445e523 Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Sun, 28 Sep 2025 20:47:50 +0200 Subject: [PATCH 01/20] fix working --- src/input/input_python.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index 568c5e08d..a92935faf 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -48,6 +48,20 @@ use super::{ Input, }; +static ORDERED_DICT_TYPE: PyOnceLock> = PyOnceLock::new(); + +fn get_ordered_dict_type(py: Python<'_>) -> &Bound<'_, PyType> { + ORDERED_DICT_TYPE + .get_or_init(py, || { + py.import("collections") + .and_then(|collections_module| collections_module.getattr("OrderedDict")) + .unwrap() + .extract() + .unwrap() + }) + .bind(py) +} + static FRACTION_TYPE: PyOnceLock> = PyOnceLock::new(); pub fn get_fraction_type(py: Python<'_>) -> &Bound<'_, PyType> { @@ -399,6 +413,13 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { } fn lax_dict<'a>(&'a self) -> ValResult> { + let ordered_dict_type = get_ordered_dict_type(self.py()); + if self.is_instance(ordered_dict_type).unwrap_or(false) { + // OrderedDict is a subclass of dict, but we want to treat it as a mapping to preserve order + if let Ok(mapping) = self.downcast::() { + return Ok(GenericPyMapping::Mapping(mapping)); + } + } if let Ok(dict) = self.downcast::() { Ok(GenericPyMapping::Dict(dict)) } else if let Ok(mapping) = self.downcast::() { From f5c2042a826c93ea9b96d12bdd836bded719405c Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Tue, 30 Sep 2025 21:31:06 +0200 Subject: [PATCH 02/20] WIP: add test --- src/input/input_python.rs | 19 +++++++++++++++++-- tests/validators/test_dict.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index a92935faf..f7f165ad7 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -62,6 +62,20 @@ fn get_ordered_dict_type(py: Python<'_>) -> &Bound<'_, PyType> { .bind(py) } +// Lazy helper that only initializes OrderedDict type when actually needed +fn check_if_ordered_dict(obj: &Bound<'_, PyAny>) -> bool { + // Quick type name check first - avoid Python import if possible + if let Ok(type_name) = obj.get_type().name() { + if type_name.to_string() != "OrderedDict" { + return false; // Fast path for non-OrderedDict objects + } + } + + // Only now do we need the expensive type lookup + let ordered_dict_type = get_ordered_dict_type(obj.py()); + obj.is_instance(ordered_dict_type).unwrap_or(false) +} + static FRACTION_TYPE: PyOnceLock> = PyOnceLock::new(); pub fn get_fraction_type(py: Python<'_>) -> &Bound<'_, PyType> { @@ -413,13 +427,14 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { } fn lax_dict<'a>(&'a self) -> ValResult> { - let ordered_dict_type = get_ordered_dict_type(self.py()); - if self.is_instance(ordered_dict_type).unwrap_or(false) { + // Optimized: Only check for OrderedDict when needed + if check_if_ordered_dict(self) { // OrderedDict is a subclass of dict, but we want to treat it as a mapping to preserve order if let Ok(mapping) = self.downcast::() { return Ok(GenericPyMapping::Mapping(mapping)); } } + if let Ok(dict) = self.downcast::() { Ok(GenericPyMapping::Dict(dict)) } else if let Ok(mapping) = self.downcast::() { diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index bff6cf5a3..c31f4ea74 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -255,3 +255,34 @@ def test_json_dict_complex_key(): assert v.validate_json('{"1+2j": 2, "infj": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4} with pytest.raises(ValidationError, match='Input should be a valid complex string'): v.validate_json('{"1+2j": 2, "": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4} + + +... +def test_ordered_dict_key_order_preservation(): + # GH 12273 + v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema())) + + # Test case from original issue + foo = OrderedDict({"a": 1, "b": 2}) + foo.move_to_end("a") + + result = v.validate_python(foo) + assert list(result.keys()) == list(foo.keys()) == ['b', 'a'] + assert result == {'b': 2, 'a': 1} + + # Test with more complex reordering + foo2 = OrderedDict({"x": 1, "y": 2, "z": 3}) + foo2.move_to_end("x") + + result2 = v.validate_python(foo2) + assert list(result2.keys()) == list(foo2.keys()) == ['y', 'z', 'x'] + assert result2 == {'y': 2, 'z': 3, 'x': 1} + + # Test popitem and re-insertion + foo3 = OrderedDict({"p": 1, "q": 2}) + item = foo3.popitem(last=False) + foo3[item[0]] = item[1] + + result3 = v.validate_python(foo3) + assert list(result3.keys()) == list(foo3.keys()) == ['q', 'p'] + assert result3 == {'q': 2, 'p': 1} From 4f88e03994147243556901e6fb7b051990c48ac2 Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Tue, 30 Sep 2025 21:33:13 +0200 Subject: [PATCH 03/20] simplify test case --- tests/validators/test_dict.py | 300 +++++++++++++++++++++------------- 1 file changed, 186 insertions(+), 114 deletions(-) diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index c31f4ea74..972a0bbdb 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -13,33 +13,62 @@ def test_dict(py_and_json: PyAndJson): - v = py_and_json({'type': 'dict', 'keys_schema': {'type': 'int'}, 'values_schema': {'type': 'int'}}) - assert v.validate_test({'1': 2, '3': 4}) == {1: 2, 3: 4} - v = py_and_json({'type': 'dict', 'strict': True, 'keys_schema': {'type': 'int'}, 'values_schema': {'type': 'int'}}) - assert v.validate_test({'1': 2, '3': 4}) == {1: 2, 3: 4} + v = py_and_json( + { + "type": "dict", + "keys_schema": {"type": "int"}, + "values_schema": {"type": "int"}, + } + ) + assert v.validate_test({"1": 2, "3": 4}) == {1: 2, 3: 4} + v = py_and_json( + { + "type": "dict", + "strict": True, + "keys_schema": {"type": "int"}, + "values_schema": {"type": "int"}, + } + ) + assert v.validate_test({"1": 2, "3": 4}) == {1: 2, 3: 4} assert v.validate_test({}) == {} - with pytest.raises(ValidationError, match=re.escape('[type=dict_type, input_value=[], input_type=list]')): + with pytest.raises( + ValidationError, + match=re.escape("[type=dict_type, input_value=[], input_type=list]"), + ): v.validate_test([]) @pytest.mark.parametrize( - 'input_value,expected', + "input_value,expected", [ - ({'1': b'1', '2': b'2'}, {'1': '1', '2': '2'}), - (OrderedDict(a=b'1', b='2'), {'a': '1', 'b': '2'}), + ({"1": b"1", "2": b"2"}, {"1": "1", "2": "2"}), + (OrderedDict(a=b"1", b="2"), {"a": "1", "b": "2"}), ({}, {}), - ('foobar', Err("Input should be a valid dictionary [type=dict_type, input_value='foobar', input_type=str]")), - ([], Err('Input should be a valid dictionary [type=dict_type,')), - ([('x', 'y')], Err('Input should be a valid dictionary [type=dict_type,')), - ([('x', 'y'), ('z', 'z')], Err('Input should be a valid dictionary [type=dict_type,')), - ((), Err('Input should be a valid dictionary [type=dict_type,')), - ((('x', 'y'),), Err('Input should be a valid dictionary [type=dict_type,')), - ((type('Foobar', (), {'x': 1})()), Err('Input should be a valid dictionary [type=dict_type,')), + ( + "foobar", + Err( + "Input should be a valid dictionary [type=dict_type, input_value='foobar', input_type=str]" + ), + ), + ([], Err("Input should be a valid dictionary [type=dict_type,")), + ([("x", "y")], Err("Input should be a valid dictionary [type=dict_type,")), + ( + [("x", "y"), ("z", "z")], + Err("Input should be a valid dictionary [type=dict_type,"), + ), + ((), Err("Input should be a valid dictionary [type=dict_type,")), + ((("x", "y"),), Err("Input should be a valid dictionary [type=dict_type,")), + ( + (type("Foobar", (), {"x": 1})()), + Err("Input should be a valid dictionary [type=dict_type,"), + ), ], ids=repr, ) def test_dict_cases(input_value, expected): - v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.str_schema())) + v = SchemaValidator( + cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.str_schema()) + ) if isinstance(expected, Err): with pytest.raises(ValidationError, match=re.escape(expected.message)): v.validate_python(input_value) @@ -48,51 +77,57 @@ def test_dict_cases(input_value, expected): def test_dict_value_error(py_and_json: PyAndJson): - v = py_and_json({'type': 'dict', 'values_schema': {'type': 'int'}}) - assert v.validate_test({'a': 2, 'b': '4'}) == {'a': 2, 'b': 4} - with pytest.raises(ValidationError, match='Input should be a valid integer') as exc_info: - v.validate_test({'a': 2, 'b': 'wrong'}) + v = py_and_json({"type": "dict", "values_schema": {"type": "int"}}) + assert v.validate_test({"a": 2, "b": "4"}) == {"a": 2, "b": 4} + with pytest.raises( + ValidationError, match="Input should be a valid integer" + ) as exc_info: + v.validate_test({"a": 2, "b": "wrong"}) assert exc_info.value.errors(include_url=False) == [ { - 'type': 'int_parsing', - 'loc': ('b',), - 'msg': 'Input should be a valid integer, unable to parse string as an integer', - 'input': 'wrong', + "type": "int_parsing", + "loc": ("b",), + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "wrong", } ] def test_dict_error_key_int(): v = SchemaValidator(cs.dict_schema(values_schema=cs.int_schema())) - with pytest.raises(ValidationError, match='Input should be a valid integer') as exc_info: - v.validate_python({1: 2, 3: 'wrong', -4: 'wrong2'}) + with pytest.raises( + ValidationError, match="Input should be a valid integer" + ) as exc_info: + v.validate_python({1: 2, 3: "wrong", -4: "wrong2"}) # insert_assert(exc_info.value.errors(include_url=False)) assert exc_info.value.errors(include_url=False) == [ { - 'type': 'int_parsing', - 'loc': (3,), - 'msg': 'Input should be a valid integer, unable to parse string as an integer', - 'input': 'wrong', + "type": "int_parsing", + "loc": (3,), + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "wrong", }, { - 'type': 'int_parsing', - 'loc': (-4,), - 'msg': 'Input should be a valid integer, unable to parse string as an integer', - 'input': 'wrong2', + "type": "int_parsing", + "loc": (-4,), + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "wrong2", }, ] def test_dict_error_key_other(): v = SchemaValidator(cs.dict_schema(values_schema=cs.int_schema())) - with pytest.raises(ValidationError, match='Input should be a valid integer') as exc_info: - v.validate_python({1: 2, (1, 2): 'wrong'}) + with pytest.raises( + ValidationError, match="Input should be a valid integer" + ) as exc_info: + v.validate_python({1: 2, (1, 2): "wrong"}) assert exc_info.value.errors(include_url=False) == [ { - 'type': 'int_parsing', - 'loc': ('(1, 2)',), - 'msg': 'Input should be a valid integer, unable to parse string as an integer', - 'input': 'wrong', + "type": "int_parsing", + "loc": ("(1, 2)",), + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "wrong", } ] @@ -100,7 +135,11 @@ def test_dict_error_key_other(): def test_dict_any_value(): v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema())) v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema())) - assert v.validate_python({'1': 1, '2': 'a', '3': None}) == {'1': 1, '2': 'a', '3': None} + assert v.validate_python({"1": 1, "2": "a", "3": None}) == { + "1": 1, + "2": "a", + "3": None, + } def test_mapping(): @@ -117,24 +156,34 @@ def __iter__(self): def __len__(self): return len(self._d) - v = SchemaValidator(cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema())) - assert v.validate_python(MyMapping({'1': 2, 3: '4'})) == {1: 2, 3: 4} - v = SchemaValidator(cs.dict_schema(strict=True, keys_schema=cs.int_schema(), values_schema=cs.int_schema())) - with pytest.raises(ValidationError, match='Input should be a valid dictionary'): - v.validate_python(MyMapping({'1': 2, 3: '4'})) + v = SchemaValidator( + cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema()) + ) + assert v.validate_python(MyMapping({"1": 2, 3: "4"})) == {1: 2, 3: 4} + v = SchemaValidator( + cs.dict_schema( + strict=True, keys_schema=cs.int_schema(), values_schema=cs.int_schema() + ) + ) + with pytest.raises(ValidationError, match="Input should be a valid dictionary"): + v.validate_python(MyMapping({"1": 2, 3: "4"})) def test_key_error(): - v = SchemaValidator(cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema())) - assert v.validate_python({'1': True}) == {1: 1} - with pytest.raises(ValidationError, match=re.escape('x.[key]\n Input should be a valid integer')) as exc_info: - v.validate_python({'x': 1}) + v = SchemaValidator( + cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema()) + ) + assert v.validate_python({"1": True}) == {1: 1} + with pytest.raises( + ValidationError, match=re.escape("x.[key]\n Input should be a valid integer") + ) as exc_info: + v.validate_python({"x": 1}) assert exc_info.value.errors(include_url=False) == [ { - 'type': 'int_parsing', - 'loc': ('x', '[key]'), - 'msg': 'Input should be a valid integer, unable to parse string as an integer', - 'input': 'x', + "type": "int_parsing", + "loc": ("x", "[key]"), + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "x", } ] @@ -145,75 +194,83 @@ def __getitem__(self, key): raise None def __iter__(self): - raise RuntimeError('intentional error') + raise RuntimeError("intentional error") def __len__(self): return 1 - v = SchemaValidator(cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema())) + v = SchemaValidator( + cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema()) + ) with pytest.raises(ValidationError) as exc_info: v.validate_python(BadMapping()) assert exc_info.value.errors(include_url=False) == [ { - 'type': 'mapping_type', - 'loc': (), - 'msg': 'Input should be a valid mapping, error: RuntimeError: intentional error', - 'input': HasRepr(IsStr(regex='.+BadMapping object at.+')), - 'ctx': {'error': 'RuntimeError: intentional error'}, + "type": "mapping_type", + "loc": (), + "msg": "Input should be a valid mapping, error: RuntimeError: intentional error", + "input": HasRepr(IsStr(regex=".+BadMapping object at.+")), + "ctx": {"error": "RuntimeError: intentional error"}, } ] -@pytest.mark.parametrize('mapping_items', [[(1,)], ['foobar'], [(1, 2, 3)], 'not list']) +@pytest.mark.parametrize("mapping_items", [[(1,)], ["foobar"], [(1, 2, 3)], "not list"]) def test_mapping_error_yield_1(mapping_items): class BadMapping(Mapping): def items(self): return mapping_items def __iter__(self): - pytest.fail('unexpected call to __iter__') + pytest.fail("unexpected call to __iter__") def __getitem__(self, key): - pytest.fail('unexpected call to __getitem__') + pytest.fail("unexpected call to __getitem__") def __len__(self): return 1 - v = SchemaValidator(cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema())) + v = SchemaValidator( + cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema()) + ) with pytest.raises(ValidationError) as exc_info: v.validate_python(BadMapping()) assert exc_info.value.errors(include_url=False) == [ { - 'type': 'mapping_type', - 'loc': (), - 'msg': 'Input should be a valid mapping, error: Mapping items must be tuples of (key, value) pairs', - 'input': HasRepr(IsStr(regex='.+BadMapping object at.+')), - 'ctx': {'error': 'Mapping items must be tuples of (key, value) pairs'}, + "type": "mapping_type", + "loc": (), + "msg": "Input should be a valid mapping, error: Mapping items must be tuples of (key, value) pairs", + "input": HasRepr(IsStr(regex=".+BadMapping object at.+")), + "ctx": {"error": "Mapping items must be tuples of (key, value) pairs"}, } ] @pytest.mark.parametrize( - 'kwargs,input_value,expected', + "kwargs,input_value,expected", [ - ({}, {'1': 1, '2': 2}, {'1': 1, '2': 2}), + ({}, {"1": 1, "2": 2}, {"1": 1, "2": 2}), ( - {'min_length': 3}, - {'1': 1, '2': 2, '3': 3.0, '4': [1, 2, 3, 4]}, - {'1': 1, '2': 2, '3': 3.0, '4': [1, 2, 3, 4]}, + {"min_length": 3}, + {"1": 1, "2": 2, "3": 3.0, "4": [1, 2, 3, 4]}, + {"1": 1, "2": 2, "3": 3.0, "4": [1, 2, 3, 4]}, ), ( - {'min_length': 3}, - {1: '2', 3: '4'}, - Err('Dictionary should have at least 3 items after validation, not 2 [type=too_short,'), + {"min_length": 3}, + {1: "2", 3: "4"}, + Err( + "Dictionary should have at least 3 items after validation, not 2 [type=too_short," + ), ), - ({'max_length': 4}, {'1': 1, '2': 2, '3': 3.0}, {'1': 1, '2': 2, '3': 3.0}), + ({"max_length": 4}, {"1": 1, "2": 2, "3": 3.0}, {"1": 1, "2": 2, "3": 3.0}), ( - {'max_length': 3}, - {'1': 1, '2': 2, '3': 3.0, '4': [1, 2, 3, 4]}, - Err('Dictionary should have at most 3 items after validation, not 4 [type=too_long,'), + {"max_length": 3}, + {"1": 1, "2": 2, "3": 3.0, "4": [1, 2, 3, 4]}, + Err( + "Dictionary should have at most 3 items after validation, not 4 [type=too_long," + ), ), ], ) @@ -227,62 +284,77 @@ def test_dict_length_constraints(kwargs: dict[str, Any], input_value, expected): def test_json_dict(): - v = SchemaValidator(cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema())) + v = SchemaValidator( + cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema()) + ) assert v.validate_json('{"1": 2, "3": 4}') == {1: 2, 3: 4} with pytest.raises(ValidationError) as exc_info: - v.validate_json('1') + v.validate_json("1") assert exc_info.value.errors(include_url=False) == [ - {'type': 'dict_type', 'loc': (), 'msg': 'Input should be an object', 'input': 1} + {"type": "dict_type", "loc": (), "msg": "Input should be an object", "input": 1} ] def test_dict_complex_key(): - v = SchemaValidator(cs.dict_schema(keys_schema=cs.complex_schema(strict=True), values_schema=cs.str_schema())) - assert v.validate_python({complex(1, 2): '1'}) == {complex(1, 2): '1'} - with pytest.raises(ValidationError, match='Input should be an instance of complex'): - assert v.validate_python({'1+2j': b'1'}) == {complex(1, 2): '1'} - - v = SchemaValidator(cs.dict_schema(keys_schema=cs.complex_schema(), values_schema=cs.str_schema())) + v = SchemaValidator( + cs.dict_schema( + keys_schema=cs.complex_schema(strict=True), values_schema=cs.str_schema() + ) + ) + assert v.validate_python({complex(1, 2): "1"}) == {complex(1, 2): "1"} + with pytest.raises(ValidationError, match="Input should be an instance of complex"): + assert v.validate_python({"1+2j": b"1"}) == {complex(1, 2): "1"} + + v = SchemaValidator( + cs.dict_schema(keys_schema=cs.complex_schema(), values_schema=cs.str_schema()) + ) with pytest.raises( - ValidationError, match='Input should be a valid python complex object, a number, or a valid complex string' + ValidationError, + match="Input should be a valid python complex object, a number, or a valid complex string", ): - v.validate_python({'1+2ja': b'1'}) + v.validate_python({"1+2ja": b"1"}) def test_json_dict_complex_key(): - v = SchemaValidator(cs.dict_schema(keys_schema=cs.complex_schema(), values_schema=cs.int_schema())) - assert v.validate_json('{"1+2j": 2, "-3": 4}') == {complex(1, 2): 2, complex(-3, 0): 4} - assert v.validate_json('{"1+2j": 2, "infj": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4} - with pytest.raises(ValidationError, match='Input should be a valid complex string'): - v.validate_json('{"1+2j": 2, "": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4} + v = SchemaValidator( + cs.dict_schema(keys_schema=cs.complex_schema(), values_schema=cs.int_schema()) + ) + assert v.validate_json('{"1+2j": 2, "-3": 4}') == { + complex(1, 2): 2, + complex(-3, 0): 4, + } + assert v.validate_json('{"1+2j": 2, "infj": 4}') == { + complex(1, 2): 2, + complex(0, float("inf")): 4, + } + with pytest.raises(ValidationError, match="Input should be a valid complex string"): + v.validate_json('{"1+2j": 2, "": 4}') == { + complex(1, 2): 2, + complex(0, float("inf")): 4, + } ... + + def test_ordered_dict_key_order_preservation(): # GH 12273 - v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema())) + v = SchemaValidator( + cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema()) + ) - # Test case from original issue + # Original issue foo = OrderedDict({"a": 1, "b": 2}) foo.move_to_end("a") result = v.validate_python(foo) - assert list(result.keys()) == list(foo.keys()) == ['b', 'a'] - assert result == {'b': 2, 'a': 1} + assert list(result.keys()) == list(foo.keys()) == ["b", "a"] + assert result == {"b": 2, "a": 1} - # Test with more complex reordering + # More complex case foo2 = OrderedDict({"x": 1, "y": 2, "z": 3}) foo2.move_to_end("x") result2 = v.validate_python(foo2) - assert list(result2.keys()) == list(foo2.keys()) == ['y', 'z', 'x'] - assert result2 == {'y': 2, 'z': 3, 'x': 1} - - # Test popitem and re-insertion - foo3 = OrderedDict({"p": 1, "q": 2}) - item = foo3.popitem(last=False) - foo3[item[0]] = item[1] - - result3 = v.validate_python(foo3) - assert list(result3.keys()) == list(foo3.keys()) == ['q', 'p'] - assert result3 == {'q': 2, 'p': 1} + assert list(result2.keys()) == list(foo2.keys()) == ["y", "z", "x"] + assert result2 == {"y": 2, "z": 3, "x": 1} From 21d83a355848dce1c1be366ca02f818ffe428792 Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Tue, 30 Sep 2025 21:53:15 +0200 Subject: [PATCH 04/20] speed up checks --- src/input/input_python.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index f7f165ad7..d94e727d0 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -62,16 +62,21 @@ fn get_ordered_dict_type(py: Python<'_>) -> &Bound<'_, PyType> { .bind(py) } -// Lazy helper that only initializes OrderedDict type when actually needed +// Ultra-fast OrderedDict detection with multiple optimization layers fn check_if_ordered_dict(obj: &Bound<'_, PyAny>) -> bool { - // Quick type name check first - avoid Python import if possible + // FASTEST PATH: Check if it's exact PyDict first - skip everything for regular dicts + if obj.is_exact_instance_of::() { + return false; // Regular dict - absolutely not OrderedDict + } + + // FAST PATH: Quick type name check - avoid Python import if possible if let Ok(type_name) = obj.get_type().name() { if type_name.to_string() != "OrderedDict" { - return false; // Fast path for non-OrderedDict objects + return false; // Not OrderedDict based on name } } - // Only now do we need the expensive type lookup + // SLOW PATH: Only for actual OrderedDict objects - expensive type lookup let ordered_dict_type = get_ordered_dict_type(obj.py()); obj.is_instance(ordered_dict_type).unwrap_or(false) } @@ -427,7 +432,6 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { } fn lax_dict<'a>(&'a self) -> ValResult> { - // Optimized: Only check for OrderedDict when needed if check_if_ordered_dict(self) { // OrderedDict is a subclass of dict, but we want to treat it as a mapping to preserve order if let Ok(mapping) = self.downcast::() { From 6e35fe9b9eff1ce0c45653a35a3792b3adf9b32f Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Tue, 30 Sep 2025 21:58:27 +0200 Subject: [PATCH 05/20] optimize code --- src/input/input_python.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index d94e727d0..a48064137 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -62,21 +62,17 @@ fn get_ordered_dict_type(py: Python<'_>) -> &Bound<'_, PyType> { .bind(py) } -// Ultra-fast OrderedDict detection with multiple optimization layers fn check_if_ordered_dict(obj: &Bound<'_, PyAny>) -> bool { - // FASTEST PATH: Check if it's exact PyDict first - skip everything for regular dicts if obj.is_exact_instance_of::() { - return false; // Regular dict - absolutely not OrderedDict + return false; } - // FAST PATH: Quick type name check - avoid Python import if possible if let Ok(type_name) = obj.get_type().name() { if type_name.to_string() != "OrderedDict" { - return false; // Not OrderedDict based on name + return false; } } - // SLOW PATH: Only for actual OrderedDict objects - expensive type lookup let ordered_dict_type = get_ordered_dict_type(obj.py()); obj.is_instance(ordered_dict_type).unwrap_or(false) } @@ -432,6 +428,7 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { } fn lax_dict<'a>(&'a self) -> ValResult> { + if check_if_ordered_dict(self) { // OrderedDict is a subclass of dict, but we want to treat it as a mapping to preserve order if let Ok(mapping) = self.downcast::() { From 1b30fbc3232cae8b10a8bc6b39fbec92fb782470 Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Tue, 30 Sep 2025 22:08:40 +0200 Subject: [PATCH 06/20] update test dict --- tests/validators/test_dict.py | 352 +++++++++++----------------------- 1 file changed, 116 insertions(+), 236 deletions(-) diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 591cb91d4..b1ea0b637 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -15,60 +15,56 @@ def test_dict(py_and_json: PyAndJson): v = py_and_json( { - "type": "dict", - "keys_schema": {"type": "int"}, - "values_schema": {"type": "int"}, + 'type': 'dict', + 'keys_schema': {'type': 'int'}, + 'values_schema': {'type': 'int'}, } ) - assert v.validate_test({"1": 2, "3": 4}) == {1: 2, 3: 4} + assert v.validate_test({'1': 2, '3': 4}) == {1: 2, 3: 4} v = py_and_json( { - "type": "dict", - "strict": True, - "keys_schema": {"type": "int"}, - "values_schema": {"type": "int"}, + 'type': 'dict', + 'strict': True, + 'keys_schema': {'type': 'int'}, + 'values_schema': {'type': 'int'}, } ) - assert v.validate_test({"1": 2, "3": 4}) == {1: 2, 3: 4} + assert v.validate_test({'1': 2, '3': 4}) == {1: 2, 3: 4} assert v.validate_test({}) == {} with pytest.raises( ValidationError, - match=re.escape("[type=dict_type, input_value=[], input_type=list]"), + match=re.escape('[type=dict_type, input_value=[], input_type=list]'), ): v.validate_test([]) @pytest.mark.parametrize( - "input_value,expected", + 'input_value,expected', [ - ({"1": b"1", "2": b"2"}, {"1": "1", "2": "2"}), - (OrderedDict(a=b"1", b="2"), {"a": "1", "b": "2"}), + ({'1': b'1', '2': b'2'}, {'1': '1', '2': '2'}), + (OrderedDict(a=b'1', b='2'), {'a': '1', 'b': '2'}), ({}, {}), ( - "foobar", - Err( - "Input should be a valid dictionary [type=dict_type, input_value='foobar', input_type=str]" - ), + 'foobar', + Err("Input should be a valid dictionary [type=dict_type, input_value='foobar', input_type=str]"), ), - ([], Err("Input should be a valid dictionary [type=dict_type,")), - ([("x", "y")], Err("Input should be a valid dictionary [type=dict_type,")), + ([], Err('Input should be a valid dictionary [type=dict_type,')), + ([('x', 'y')], Err('Input should be a valid dictionary [type=dict_type,')), ( - [("x", "y"), ("z", "z")], - Err("Input should be a valid dictionary [type=dict_type,"), + [('x', 'y'), ('z', 'z')], + Err('Input should be a valid dictionary [type=dict_type,'), ), - ((), Err("Input should be a valid dictionary [type=dict_type,")), - ((("x", "y"),), Err("Input should be a valid dictionary [type=dict_type,")), + ((), Err('Input should be a valid dictionary [type=dict_type,')), + ((('x', 'y'),), Err('Input should be a valid dictionary [type=dict_type,')), ( - (type("Foobar", (), {"x": 1})()), - Err("Input should be a valid dictionary [type=dict_type,"), + (type('Foobar', (), {'x': 1})()), + Err('Input should be a valid dictionary [type=dict_type,'), ), ], ids=repr, ) def test_dict_cases(input_value, expected): - v = SchemaValidator( - cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.str_schema()) - ) + v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.str_schema())) if isinstance(expected, Err): with pytest.raises(ValidationError, match=re.escape(expected.message)): v.validate_python(input_value) @@ -77,57 +73,51 @@ def test_dict_cases(input_value, expected): def test_dict_value_error(py_and_json: PyAndJson): - v = py_and_json({"type": "dict", "values_schema": {"type": "int"}}) - assert v.validate_test({"a": 2, "b": "4"}) == {"a": 2, "b": 4} - with pytest.raises( - ValidationError, match="Input should be a valid integer" - ) as exc_info: - v.validate_test({"a": 2, "b": "wrong"}) + v = py_and_json({'type': 'dict', 'values_schema': {'type': 'int'}}) + assert v.validate_test({'a': 2, 'b': '4'}) == {'a': 2, 'b': 4} + with pytest.raises(ValidationError, match='Input should be a valid integer') as exc_info: + v.validate_test({'a': 2, 'b': 'wrong'}) assert exc_info.value.errors(include_url=False) == [ { - "type": "int_parsing", - "loc": ("b",), - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "wrong", + 'type': 'int_parsing', + 'loc': ('b',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'wrong', } ] def test_dict_error_key_int(): v = SchemaValidator(cs.dict_schema(values_schema=cs.int_schema())) - with pytest.raises( - ValidationError, match="Input should be a valid integer" - ) as exc_info: - v.validate_python({1: 2, 3: "wrong", -4: "wrong2"}) + with pytest.raises(ValidationError, match='Input should be a valid integer') as exc_info: + v.validate_python({1: 2, 3: 'wrong', -4: 'wrong2'}) # insert_assert(exc_info.value.errors(include_url=False)) assert exc_info.value.errors(include_url=False) == [ { - "type": "int_parsing", - "loc": (3,), - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "wrong", + 'type': 'int_parsing', + 'loc': (3,), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'wrong', }, { - "type": "int_parsing", - "loc": (-4,), - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "wrong2", + 'type': 'int_parsing', + 'loc': (-4,), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'wrong2', }, ] def test_dict_error_key_other(): v = SchemaValidator(cs.dict_schema(values_schema=cs.int_schema())) - with pytest.raises( - ValidationError, match="Input should be a valid integer" - ) as exc_info: - v.validate_python({1: 2, (1, 2): "wrong"}) + with pytest.raises(ValidationError, match='Input should be a valid integer') as exc_info: + v.validate_python({1: 2, (1, 2): 'wrong'}) assert exc_info.value.errors(include_url=False) == [ { - "type": "int_parsing", - "loc": ("(1, 2)",), - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "wrong", + 'type': 'int_parsing', + 'loc': ('(1, 2)',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'wrong', } ] @@ -135,10 +125,10 @@ def test_dict_error_key_other(): def test_dict_any_value(): v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema())) v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema())) - assert v.validate_python({"1": 1, "2": "a", "3": None}) == { - "1": 1, - "2": "a", - "3": None, + assert v.validate_python({'1': 1, '2': 'a', '3': None}) == { + '1': 1, + '2': 'a', + '3': None, } @@ -156,34 +146,24 @@ def __iter__(self): def __len__(self): return len(self._d) - v = SchemaValidator( - cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema()) - ) - assert v.validate_python(MyMapping({"1": 2, 3: "4"})) == {1: 2, 3: 4} - v = SchemaValidator( - cs.dict_schema( - strict=True, keys_schema=cs.int_schema(), values_schema=cs.int_schema() - ) - ) - with pytest.raises(ValidationError, match="Input should be a valid dictionary"): - v.validate_python(MyMapping({"1": 2, 3: "4"})) + v = SchemaValidator(cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema())) + assert v.validate_python(MyMapping({'1': 2, 3: '4'})) == {1: 2, 3: 4} + v = SchemaValidator(cs.dict_schema(strict=True, keys_schema=cs.int_schema(), values_schema=cs.int_schema())) + with pytest.raises(ValidationError, match='Input should be a valid dictionary'): + v.validate_python(MyMapping({'1': 2, 3: '4'})) def test_key_error(): - v = SchemaValidator( - cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema()) - ) - assert v.validate_python({"1": True}) == {1: 1} - with pytest.raises( - ValidationError, match=re.escape("x.[key]\n Input should be a valid integer") - ) as exc_info: - v.validate_python({"x": 1}) + v = SchemaValidator(cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema())) + assert v.validate_python({'1': True}) == {1: 1} + with pytest.raises(ValidationError, match=re.escape('x.[key]\n Input should be a valid integer')) as exc_info: + v.validate_python({'x': 1}) assert exc_info.value.errors(include_url=False) == [ { - "type": "int_parsing", - "loc": ("x", "[key]"), - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "x", + 'type': 'int_parsing', + 'loc': ('x', '[key]'), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'x', } ] @@ -194,83 +174,75 @@ def __getitem__(self, key): raise None def __iter__(self): - raise RuntimeError("intentional error") + raise RuntimeError('intentional error') def __len__(self): return 1 - v = SchemaValidator( - cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema()) - ) + v = SchemaValidator(cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema())) with pytest.raises(ValidationError) as exc_info: v.validate_python(BadMapping()) assert exc_info.value.errors(include_url=False) == [ { - "type": "mapping_type", - "loc": (), - "msg": "Input should be a valid mapping, error: RuntimeError: intentional error", - "input": HasRepr(IsStr(regex=".+BadMapping object at.+")), - "ctx": {"error": "RuntimeError: intentional error"}, + 'type': 'mapping_type', + 'loc': (), + 'msg': 'Input should be a valid mapping, error: RuntimeError: intentional error', + 'input': HasRepr(IsStr(regex='.+BadMapping object at.+')), + 'ctx': {'error': 'RuntimeError: intentional error'}, } ] -@pytest.mark.parametrize("mapping_items", [[(1,)], ["foobar"], [(1, 2, 3)], "not list"]) +@pytest.mark.parametrize('mapping_items', [[(1,)], ['foobar'], [(1, 2, 3)], 'not list']) def test_mapping_error_yield_1(mapping_items): class BadMapping(Mapping): def items(self): return mapping_items def __iter__(self): - pytest.fail("unexpected call to __iter__") + pytest.fail('unexpected call to __iter__') def __getitem__(self, key): - pytest.fail("unexpected call to __getitem__") + pytest.fail('unexpected call to __getitem__') def __len__(self): return 1 - v = SchemaValidator( - cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema()) - ) + v = SchemaValidator(cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema())) with pytest.raises(ValidationError) as exc_info: v.validate_python(BadMapping()) assert exc_info.value.errors(include_url=False) == [ { - "type": "mapping_type", - "loc": (), - "msg": "Input should be a valid mapping, error: Mapping items must be tuples of (key, value) pairs", - "input": HasRepr(IsStr(regex=".+BadMapping object at.+")), - "ctx": {"error": "Mapping items must be tuples of (key, value) pairs"}, + 'type': 'mapping_type', + 'loc': (), + 'msg': 'Input should be a valid mapping, error: Mapping items must be tuples of (key, value) pairs', + 'input': HasRepr(IsStr(regex='.+BadMapping object at.+')), + 'ctx': {'error': 'Mapping items must be tuples of (key, value) pairs'}, } ] @pytest.mark.parametrize( - "kwargs,input_value,expected", + 'kwargs,input_value,expected', [ - ({}, {"1": 1, "2": 2}, {"1": 1, "2": 2}), + ({}, {'1': 1, '2': 2}, {'1': 1, '2': 2}), ( - {"min_length": 3}, - {"1": 1, "2": 2, "3": 3.0, "4": [1, 2, 3, 4]}, - {"1": 1, "2": 2, "3": 3.0, "4": [1, 2, 3, 4]}, + {'min_length': 3}, + {'1': 1, '2': 2, '3': 3.0, '4': [1, 2, 3, 4]}, + {'1': 1, '2': 2, '3': 3.0, '4': [1, 2, 3, 4]}, ), ( - {"min_length": 3}, - {1: "2", 3: "4"}, - Err( - "Dictionary should have at least 3 items after validation, not 2 [type=too_short," - ), + {'min_length': 3}, + {1: '2', 3: '4'}, + Err('Dictionary should have at least 3 items after validation, not 2 [type=too_short,'), ), - ({"max_length": 4}, {"1": 1, "2": 2, "3": 3.0}, {"1": 1, "2": 2, "3": 3.0}), + ({'max_length': 4}, {'1': 1, '2': 2, '3': 3.0}, {'1': 1, '2': 2, '3': 3.0}), ( - {"max_length": 3}, - {"1": 1, "2": 2, "3": 3.0, "4": [1, 2, 3, 4]}, - Err( - "Dictionary should have at most 3 items after validation, not 4 [type=too_long," - ), + {'max_length': 3}, + {'1': 1, '2': 2, '3': 3.0, '4': [1, 2, 3, 4]}, + Err('Dictionary should have at most 3 items after validation, not 4 [type=too_long,'), ), ], ) @@ -284,154 +256,62 @@ def test_dict_length_constraints(kwargs: dict[str, Any], input_value, expected): def test_json_dict(): - v = SchemaValidator( - cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema()) - ) + v = SchemaValidator(cs.dict_schema(keys_schema=cs.int_schema(), values_schema=cs.int_schema())) assert v.validate_json('{"1": 2, "3": 4}') == {1: 2, 3: 4} with pytest.raises(ValidationError) as exc_info: - v.validate_json("1") + v.validate_json('1') assert exc_info.value.errors(include_url=False) == [ - {"type": "dict_type", "loc": (), "msg": "Input should be an object", "input": 1} + {'type': 'dict_type', 'loc': (), 'msg': 'Input should be an object', 'input': 1} ] def test_dict_complex_key(): - v = SchemaValidator( - cs.dict_schema( - keys_schema=cs.complex_schema(strict=True), values_schema=cs.str_schema() - ) - ) - assert v.validate_python({complex(1, 2): "1"}) == {complex(1, 2): "1"} - with pytest.raises(ValidationError, match="Input should be an instance of complex"): - assert v.validate_python({"1+2j": b"1"}) == {complex(1, 2): "1"} + v = SchemaValidator(cs.dict_schema(keys_schema=cs.complex_schema(strict=True), values_schema=cs.str_schema())) + assert v.validate_python({complex(1, 2): '1'}) == {complex(1, 2): '1'} + with pytest.raises(ValidationError, match='Input should be an instance of complex'): + assert v.validate_python({'1+2j': b'1'}) == {complex(1, 2): '1'} - v = SchemaValidator( - cs.dict_schema(keys_schema=cs.complex_schema(), values_schema=cs.str_schema()) - ) + v = SchemaValidator(cs.dict_schema(keys_schema=cs.complex_schema(), values_schema=cs.str_schema())) with pytest.raises( ValidationError, - match="Input should be a valid python complex object, a number, or a valid complex string", + match='Input should be a valid python complex object, a number, or a valid complex string', ): - v.validate_python({"1+2ja": b"1"}) + v.validate_python({'1+2ja': b'1'}) def test_json_dict_complex_key(): - v = SchemaValidator( - cs.dict_schema(keys_schema=cs.complex_schema(), values_schema=cs.int_schema()) - ) + v = SchemaValidator(cs.dict_schema(keys_schema=cs.complex_schema(), values_schema=cs.int_schema())) assert v.validate_json('{"1+2j": 2, "-3": 4}') == { complex(1, 2): 2, complex(-3, 0): 4, } assert v.validate_json('{"1+2j": 2, "infj": 4}') == { complex(1, 2): 2, - complex(0, float("inf")): 4, + complex(0, float('inf')): 4, } - with pytest.raises(ValidationError, match="Input should be a valid complex string"): + with pytest.raises(ValidationError, match='Input should be a valid complex string'): v.validate_json('{"1+2j": 2, "": 4}') == { complex(1, 2): 2, - complex(0, float("inf")): 4, - } - - v = SchemaValidator( - cs.dict_schema(keys_schema=cs.complex_schema(), values_schema=cs.int_schema()) - ) - assert v.validate_json('{"1+2j": 2, "-3": 4}') == { - complex(1, 2): 2, - complex(-3, 0): 4, - } - assert v.validate_json('{"1+2j": 2, "infj": 4}') == { - complex(1, 2): 2, - complex(0, float("inf")): 4, - } - with pytest.raises(ValidationError, match="Input should be a valid complex string"): - v.validate_json('{"1+2j": 2, "": 4}') == { - complex(1, 2): 2, - complex(0, float("inf")): 4, + complex(0, float('inf')): 4, } def test_ordered_dict_key_order_preservation(): # GH 12273 - v = SchemaValidator( - cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema()) - ) + v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema())) # Original issue - foo = OrderedDict({"a": 1, "b": 2}) - foo.move_to_end("a") + foo = OrderedDict({'a': 1, 'b': 2}) + foo.move_to_end('a') result = v.validate_python(foo) - assert list(result.keys()) == list(foo.keys()) == ["b", "a"] - assert result == {"b": 2, "a": 1} + assert list(result.keys()) == list(foo.keys()) == ['b', 'a'] + assert result == {'b': 2, 'a': 1} # More complex case - foo2 = OrderedDict({"x": 1, "y": 2, "z": 3}) - foo2.move_to_end("x") + foo2 = OrderedDict({'x': 1, 'y': 2, 'z': 3}) + foo2.move_to_end('x') result2 = v.validate_python(foo2) - assert list(result2.keys()) == list(foo2.keys()) == ["y", "z", "x"] - assert result2 == {"y": 2, "z": 3, "x": 1} - - -@pytest.mark.parametrize( - ("fail_fast", "expected"), - [ - pytest.param( - True, - [ - { - "type": "int_parsing", - "loc": ("a", "[key]"), - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "a", - }, - ], - id="fail_fast", - ), - pytest.param( - False, - [ - { - "type": "int_parsing", - "loc": ("a", "[key]"), - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "a", - }, - { - "type": "int_parsing", - "loc": ("a",), - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "b", - }, - { - "type": "int_parsing", - "loc": ("c", "[key]"), - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "c", - }, - { - "type": "int_parsing", - "loc": ("c",), - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "d", - }, - ], - id="not_fail_fast", - ), - ], -) -def test_dict_fail_fast(fail_fast, expected): - v = SchemaValidator( - { - "type": "dict", - "keys_schema": {"type": "int"}, - "values_schema": {"type": "int"}, - "fail_fast": fail_fast, - } - ) - - with pytest.raises(ValidationError) as exc_info: - v.validate_python({"a": "b", "c": "d"}) - - assert exc_info.value.errors(include_url=False) == expected + assert list(result2.keys()) == list(foo2.keys()) == ['y', 'z', 'x'] + assert result2 == {'y': 2, 'z': 3, 'x': 1} From be8ef2b2d4f4c7fa289216afdf3e1a8fb15f2633 Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Tue, 30 Sep 2025 22:14:49 +0200 Subject: [PATCH 07/20] remove auto formatting --- tests/validators/test_dict.py | 61 +++++++---------------------------- 1 file changed, 11 insertions(+), 50 deletions(-) diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index b1ea0b637..02c23b5de 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -13,28 +13,12 @@ def test_dict(py_and_json: PyAndJson): - v = py_and_json( - { - 'type': 'dict', - 'keys_schema': {'type': 'int'}, - 'values_schema': {'type': 'int'}, - } - ) + v = py_and_json({'type': 'dict', 'keys_schema': {'type': 'int'}, 'values_schema': {'type': 'int'}}) assert v.validate_test({'1': 2, '3': 4}) == {1: 2, 3: 4} - v = py_and_json( - { - 'type': 'dict', - 'strict': True, - 'keys_schema': {'type': 'int'}, - 'values_schema': {'type': 'int'}, - } - ) + v = py_and_json({'type': 'dict', 'strict': True, 'keys_schema': {'type': 'int'}, 'values_schema': {'type': 'int'}}) assert v.validate_test({'1': 2, '3': 4}) == {1: 2, 3: 4} assert v.validate_test({}) == {} - with pytest.raises( - ValidationError, - match=re.escape('[type=dict_type, input_value=[], input_type=list]'), - ): + with pytest.raises(ValidationError, match=re.escape('[type=dict_type, input_value=[], input_type=list]')): v.validate_test([]) @@ -44,22 +28,13 @@ def test_dict(py_and_json: PyAndJson): ({'1': b'1', '2': b'2'}, {'1': '1', '2': '2'}), (OrderedDict(a=b'1', b='2'), {'a': '1', 'b': '2'}), ({}, {}), - ( - 'foobar', - Err("Input should be a valid dictionary [type=dict_type, input_value='foobar', input_type=str]"), - ), + ('foobar', Err("Input should be a valid dictionary [type=dict_type, input_value='foobar', input_type=str]")), ([], Err('Input should be a valid dictionary [type=dict_type,')), ([('x', 'y')], Err('Input should be a valid dictionary [type=dict_type,')), - ( - [('x', 'y'), ('z', 'z')], - Err('Input should be a valid dictionary [type=dict_type,'), - ), + ([('x', 'y'), ('z', 'z')], Err('Input should be a valid dictionary [type=dict_type,')), ((), Err('Input should be a valid dictionary [type=dict_type,')), ((('x', 'y'),), Err('Input should be a valid dictionary [type=dict_type,')), - ( - (type('Foobar', (), {'x': 1})()), - Err('Input should be a valid dictionary [type=dict_type,'), - ), + ((type('Foobar', (), {'x': 1})()), Err('Input should be a valid dictionary [type=dict_type,')), ], ids=repr, ) @@ -125,11 +100,7 @@ def test_dict_error_key_other(): def test_dict_any_value(): v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema())) v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema())) - assert v.validate_python({'1': 1, '2': 'a', '3': None}) == { - '1': 1, - '2': 'a', - '3': None, - } + assert v.validate_python({'1': 1, '2': 'a', '3': None}) == {'1': 1, '2': 'a', '3': None} def test_mapping(): @@ -273,27 +244,17 @@ def test_dict_complex_key(): v = SchemaValidator(cs.dict_schema(keys_schema=cs.complex_schema(), values_schema=cs.str_schema())) with pytest.raises( - ValidationError, - match='Input should be a valid python complex object, a number, or a valid complex string', + ValidationError, match='Input should be a valid python complex object, a number, or a valid complex string' ): v.validate_python({'1+2ja': b'1'}) def test_json_dict_complex_key(): v = SchemaValidator(cs.dict_schema(keys_schema=cs.complex_schema(), values_schema=cs.int_schema())) - assert v.validate_json('{"1+2j": 2, "-3": 4}') == { - complex(1, 2): 2, - complex(-3, 0): 4, - } - assert v.validate_json('{"1+2j": 2, "infj": 4}') == { - complex(1, 2): 2, - complex(0, float('inf')): 4, - } + assert v.validate_json('{"1+2j": 2, "-3": 4}') == {complex(1, 2): 2, complex(-3, 0): 4} + assert v.validate_json('{"1+2j": 2, "infj": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4} with pytest.raises(ValidationError, match='Input should be a valid complex string'): - v.validate_json('{"1+2j": 2, "": 4}') == { - complex(1, 2): 2, - complex(0, float('inf')): 4, - } + v.validate_json('{"1+2j": 2, "": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4} def test_ordered_dict_key_order_preservation(): From bef11c6438900de3d8cb90e42b7cb36dfaee98b0 Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Tue, 30 Sep 2025 22:21:36 +0200 Subject: [PATCH 08/20] update test_dict to reintroduce function I accidentally deleted --- tests/validators/test_dict.py | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 02c23b5de..01e57b51c 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -257,6 +257,63 @@ def test_json_dict_complex_key(): v.validate_json('{"1+2j": 2, "": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4} +@pytest.mark.parametrize( + ('fail_fast', 'expected'), + [ + pytest.param( + True, + [ + { + 'type': 'int_parsing', + 'loc': ('a', '[key]'), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'a', + }, + ], + id='fail_fast', + ), + pytest.param( + False, + [ + { + 'type': 'int_parsing', + 'loc': ('a', '[key]'), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'a', + }, + { + 'type': 'int_parsing', + 'loc': ('a',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'b', + }, + { + 'type': 'int_parsing', + 'loc': ('c', '[key]'), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'c', + }, + { + 'type': 'int_parsing', + 'loc': ('c',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'd', + }, + ], + id='not_fail_fast', + ), + ], +) +def test_dict_fail_fast(fail_fast, expected): + v = SchemaValidator( + {'type': 'dict', 'keys_schema': {'type': 'int'}, 'values_schema': {'type': 'int'}, 'fail_fast': fail_fast} + ) + + with pytest.raises(ValidationError) as exc_info: + v.validate_python({'a': 'b', 'c': 'd'}) + + assert exc_info.value.errors(include_url=False) == expected + def test_ordered_dict_key_order_preservation(): # GH 12273 v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema())) From d52d85ba18ef722f66f0eee8ee33586bdf8596f1 Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Wed, 1 Oct 2025 19:41:32 +0200 Subject: [PATCH 09/20] fix strict and updates as request in PR --- src/input/input_python.rs | 27 ++++++++++++--------------- tests/validators/test_dict.py | 7 ++++--- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index f9c811d95..d5279c426 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -63,17 +63,21 @@ fn get_ordered_dict_type(py: Python<'_>) -> &Bound<'_, PyType> { } fn check_if_ordered_dict(obj: &Bound<'_, PyAny>) -> bool { + println!("check_if_ordered_dict: {:?}", obj); if obj.is_exact_instance_of::() { + println!("is exact dict"); return false; } if let Ok(type_name) = obj.get_type().name() { + println!("this is the type name: {}", type_name); if type_name.to_string() != "OrderedDict" { return false; } } let ordered_dict_type = get_ordered_dict_type(obj.py()); + println!("is ordered dict type: {}", ordered_dict_type); obj.is_instance(ordered_dict_type).unwrap_or(false) } @@ -424,26 +428,19 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { Self: 'a; fn strict_dict<'a>(&'a self) -> ValResult> { - if let Ok(dict) = self.downcast::() { - Ok(GenericPyMapping::Dict(dict)) - } else { + if self.is_exact_instance_of::() { + Ok(GenericPyMapping::Dict(self.downcast::()?)) + } else if check_if_ordered_dict(self) { + Ok(GenericPyMapping::Mapping(self.downcast::()?)) + } + else { Err(ValError::new(ErrorTypeDefaults::DictType, self)) } } fn lax_dict<'a>(&'a self) -> ValResult> { - - if check_if_ordered_dict(self) { - // OrderedDict is a subclass of dict, but we want to treat it as a mapping to preserve order - if let Ok(mapping) = self.downcast::() { - return Ok(GenericPyMapping::Mapping(mapping)); - } - } - - if let Ok(dict) = self.downcast::() { - Ok(GenericPyMapping::Dict(dict)) - } else if let Ok(mapping) = self.downcast::() { - Ok(GenericPyMapping::Mapping(mapping)) + if (self.is_instance_of::() || self.is_instance_of::()) { + Ok(GenericPyMapping::Mapping(self.downcast::()?)) } else { Err(ValError::new(ErrorTypeDefaults::DictType, self)) } diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 01e57b51c..2d418e78e 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -314,7 +314,8 @@ def test_dict_fail_fast(fail_fast, expected): assert exc_info.value.errors(include_url=False) == expected -def test_ordered_dict_key_order_preservation(): +@pytest.mark.parametrize('strict', [True, False]) +def test_ordered_dict_key_order_preservation(strict): # GH 12273 v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema())) @@ -322,7 +323,7 @@ def test_ordered_dict_key_order_preservation(): foo = OrderedDict({'a': 1, 'b': 2}) foo.move_to_end('a') - result = v.validate_python(foo) + result = v.validate_python(foo, strict=strict) assert list(result.keys()) == list(foo.keys()) == ['b', 'a'] assert result == {'b': 2, 'a': 1} @@ -330,6 +331,6 @@ def test_ordered_dict_key_order_preservation(): foo2 = OrderedDict({'x': 1, 'y': 2, 'z': 3}) foo2.move_to_end('x') - result2 = v.validate_python(foo2) + result2 = v.validate_python(foo2, strict=strict) assert list(result2.keys()) == list(foo2.keys()) == ['y', 'z', 'x'] assert result2 == {'y': 2, 'z': 3, 'x': 1} From 0e40c5c17f9c83bef53fcf35ab12f5980d5c797d Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Wed, 1 Oct 2025 19:55:35 +0200 Subject: [PATCH 10/20] update input python --- src/input/input_python.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index d5279c426..0e25e9700 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -63,21 +63,17 @@ fn get_ordered_dict_type(py: Python<'_>) -> &Bound<'_, PyType> { } fn check_if_ordered_dict(obj: &Bound<'_, PyAny>) -> bool { - println!("check_if_ordered_dict: {:?}", obj); if obj.is_exact_instance_of::() { - println!("is exact dict"); return false; } if let Ok(type_name) = obj.get_type().name() { - println!("this is the type name: {}", type_name); if type_name.to_string() != "OrderedDict" { return false; } } let ordered_dict_type = get_ordered_dict_type(obj.py()); - println!("is ordered dict type: {}", ordered_dict_type); obj.is_instance(ordered_dict_type).unwrap_or(false) } @@ -432,14 +428,13 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { Ok(GenericPyMapping::Dict(self.downcast::()?)) } else if check_if_ordered_dict(self) { Ok(GenericPyMapping::Mapping(self.downcast::()?)) - } - else { + } else { Err(ValError::new(ErrorTypeDefaults::DictType, self)) } } fn lax_dict<'a>(&'a self) -> ValResult> { - if (self.is_instance_of::() || self.is_instance_of::()) { + if self.is_instance_of::() || self.is_instance_of::() { Ok(GenericPyMapping::Mapping(self.downcast::()?)) } else { Err(ValError::new(ErrorTypeDefaults::DictType, self)) From c0e6b68cffd8acebdd6aae36b779e3a02feee6a9 Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Wed, 1 Oct 2025 20:07:44 +0200 Subject: [PATCH 11/20] update test_dict using ruff --- tests/validators/test_dict.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 2d418e78e..9131c2375 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -314,6 +314,7 @@ def test_dict_fail_fast(fail_fast, expected): assert exc_info.value.errors(include_url=False) == expected + @pytest.mark.parametrize('strict', [True, False]) def test_ordered_dict_key_order_preservation(strict): # GH 12273 From a8ffe29188c3052ad58e3fb3d5c085994c8664ea Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Wed, 1 Oct 2025 20:14:19 +0200 Subject: [PATCH 12/20] fix linting error on rust --- src/input/input_python.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index 0e25e9700..46fc556ea 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -68,7 +68,7 @@ fn check_if_ordered_dict(obj: &Bound<'_, PyAny>) -> bool { } if let Ok(type_name) = obj.get_type().name() { - if type_name.to_string() != "OrderedDict" { + if type_name != "OrderedDict" { return false; } } From 7767ad5ac03ac7ff7376acad52c67204692c33cf Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Wed, 1 Oct 2025 21:00:38 +0200 Subject: [PATCH 13/20] try fix performance degradation --- src/input/input_python.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index 46fc556ea..76246a650 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -434,8 +434,8 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { } fn lax_dict<'a>(&'a self) -> ValResult> { - if self.is_instance_of::() || self.is_instance_of::() { - Ok(GenericPyMapping::Mapping(self.downcast::()?)) + if let Ok(mapping) = self.downcast::() { + Ok(GenericPyMapping::Mapping(mapping)) } else { Err(ValError::new(ErrorTypeDefaults::DictType, self)) } From 90b27a170fdc68c89179625e34b0aef334876a4a Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Thu, 2 Oct 2025 21:53:37 +0200 Subject: [PATCH 14/20] fix performance issue --- src/input/input_python.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index 76246a650..f9f8937db 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -434,7 +434,11 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { } fn lax_dict<'a>(&'a self) -> ValResult> { - if let Ok(mapping) = self.downcast::() { + if check_if_ordered_dict(self) { + Ok(GenericPyMapping::Mapping(self.downcast::()?)) + } else if let Ok(dict) = self.downcast::() { + Ok(GenericPyMapping::Dict(dict)) + } else if let Ok(mapping) = self.downcast::() { Ok(GenericPyMapping::Mapping(mapping)) } else { Err(ValError::new(ErrorTypeDefaults::DictType, self)) From 84faba3a70e6cb594a5758a260770e853395bb9a Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Fri, 3 Oct 2025 16:23:13 +0200 Subject: [PATCH 15/20] simplify as discussed in PR --- src/input/input_python.rs | 39 ++++----------------------------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index f9f8937db..5a1b9a4b0 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -48,35 +48,6 @@ use super::{ Input, }; -static ORDERED_DICT_TYPE: PyOnceLock> = PyOnceLock::new(); - -fn get_ordered_dict_type(py: Python<'_>) -> &Bound<'_, PyType> { - ORDERED_DICT_TYPE - .get_or_init(py, || { - py.import("collections") - .and_then(|collections_module| collections_module.getattr("OrderedDict")) - .unwrap() - .extract() - .unwrap() - }) - .bind(py) -} - -fn check_if_ordered_dict(obj: &Bound<'_, PyAny>) -> bool { - if obj.is_exact_instance_of::() { - return false; - } - - if let Ok(type_name) = obj.get_type().name() { - if type_name != "OrderedDict" { - return false; - } - } - - let ordered_dict_type = get_ordered_dict_type(obj.py()); - obj.is_instance(ordered_dict_type).unwrap_or(false) -} - static FRACTION_TYPE: PyOnceLock> = PyOnceLock::new(); pub fn get_fraction_type(py: Python<'_>) -> &Bound<'_, PyType> { @@ -424,9 +395,9 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { Self: 'a; fn strict_dict<'a>(&'a self) -> ValResult> { - if self.is_exact_instance_of::() { - Ok(GenericPyMapping::Dict(self.downcast::()?)) - } else if check_if_ordered_dict(self) { + if let Ok(dict) = self.downcast_exact::() { + Ok(GenericPyMapping::Dict(dict)) + } else if self.is_instance_of::() { Ok(GenericPyMapping::Mapping(self.downcast::()?)) } else { Err(ValError::new(ErrorTypeDefaults::DictType, self)) @@ -434,9 +405,7 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { } fn lax_dict<'a>(&'a self) -> ValResult> { - if check_if_ordered_dict(self) { - Ok(GenericPyMapping::Mapping(self.downcast::()?)) - } else if let Ok(dict) = self.downcast::() { + if let Ok(dict) = self.downcast_exact::() { Ok(GenericPyMapping::Dict(dict)) } else if let Ok(mapping) = self.downcast::() { Ok(GenericPyMapping::Mapping(mapping)) From 376a38dda267229ff4311980288235840392befb Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Fri, 3 Oct 2025 16:43:24 +0200 Subject: [PATCH 16/20] add test --- tests/validators/test_dict.py | 53 ++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 9131c2375..57564d8ad 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -1,5 +1,6 @@ import re -from collections import OrderedDict +import sys +from collections import OrderedDict, defaultdict from collections.abc import Mapping from typing import Any @@ -11,6 +12,12 @@ from ..conftest import Err, PyAndJson +# Skip OrderedDict tests on GraalPy due to a bug in PyMapping.items() +skip_on_graalpy = pytest.mark.skipif( + sys.implementation.name == 'graalpy', + reason='GraalPy has a bug where PyMapping.items() does not preserve OrderedDict order', +) + def test_dict(py_and_json: PyAndJson): v = py_and_json({'type': 'dict', 'keys_schema': {'type': 'int'}, 'values_schema': {'type': 'int'}}) @@ -315,6 +322,7 @@ def test_dict_fail_fast(fail_fast, expected): assert exc_info.value.errors(include_url=False) == expected +@skip_on_graalpy @pytest.mark.parametrize('strict', [True, False]) def test_ordered_dict_key_order_preservation(strict): # GH 12273 @@ -335,3 +343,46 @@ def test_ordered_dict_key_order_preservation(strict): result2 = v.validate_python(foo2, strict=strict) assert list(result2.keys()) == list(foo2.keys()) == ['y', 'z', 'x'] assert result2 == {'y': 2, 'z': 3, 'x': 1} + + +@skip_on_graalpy +def test_userdefined_ordereddict(): + class MyOD(Mapping): + def __init__(self, **kwargs): + self.dict = {} + for kv in kwargs.items(): + self.dict[kv[0]] = kv[1] + + def __iter__(self): + return iter(self.dict.keys()) + + def move_to_end(self, key): + self.dict[key] = self.dict.pop(key) + + def __getitem__(self, key): + return self.dict[key] + + def __len__(self): + return len(self.dict) + + v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema())) + + foo = MyOD(**{'a': 1, 'b': 2}) + foo.move_to_end('a') + + result = v.validate_python(foo) + assert list(result.keys()) == list(foo.keys()) == ['b', 'a'] + assert result == {'b': 2, 'a': 1} + + +@pytest.mark.parametrize('strict', [True, False]) +def test_defaultdict(strict): + """Test that defaultdict is accepted and converted to regular dict""" + v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema())) + + dd = defaultdict(int, {'a': 1, 'b': 2}) + + result = v.validate_python(dd, strict=strict) + assert result == {'a': 1, 'b': 2} + assert isinstance(result, dict) + assert not isinstance(result, defaultdict) From fd28d80daa782191ce3593a7bbdfde6aed1400a6 Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Fri, 3 Oct 2025 16:58:41 +0200 Subject: [PATCH 17/20] simplify defaultdict test --- tests/validators/test_dict.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 57564d8ad..f7531fdd6 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -377,12 +377,14 @@ def __len__(self): @pytest.mark.parametrize('strict', [True, False]) def test_defaultdict(strict): - """Test that defaultdict is accepted and converted to regular dict""" v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema())) - dd = defaultdict(int, {'a': 1, 'b': 2}) + dd = defaultdict(int, {}) + # simulate move to end, since defaultdict doesn't have it + dd['a'] = 1 + dd['b'] = 2 + dd['a'] = dd.pop('a') result = v.validate_python(dd, strict=strict) - assert result == {'a': 1, 'b': 2} - assert isinstance(result, dict) - assert not isinstance(result, defaultdict) + assert list(result.keys()) == list(dd.keys()) == ['b', 'a'] + assert result == {'b': 2, 'a': 1} From 987e3b52ce84e2a114dcbf1227ff18d1cd1b7c0f Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Fri, 3 Oct 2025 17:22:52 +0200 Subject: [PATCH 18/20] remove tests that were passing before --- tests/validators/test_dict.py | 47 +---------------------------------- 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index f7531fdd6..4b332115e 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -1,6 +1,6 @@ import re import sys -from collections import OrderedDict, defaultdict +from collections import OrderedDict from collections.abc import Mapping from typing import Any @@ -343,48 +343,3 @@ def test_ordered_dict_key_order_preservation(strict): result2 = v.validate_python(foo2, strict=strict) assert list(result2.keys()) == list(foo2.keys()) == ['y', 'z', 'x'] assert result2 == {'y': 2, 'z': 3, 'x': 1} - - -@skip_on_graalpy -def test_userdefined_ordereddict(): - class MyOD(Mapping): - def __init__(self, **kwargs): - self.dict = {} - for kv in kwargs.items(): - self.dict[kv[0]] = kv[1] - - def __iter__(self): - return iter(self.dict.keys()) - - def move_to_end(self, key): - self.dict[key] = self.dict.pop(key) - - def __getitem__(self, key): - return self.dict[key] - - def __len__(self): - return len(self.dict) - - v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema())) - - foo = MyOD(**{'a': 1, 'b': 2}) - foo.move_to_end('a') - - result = v.validate_python(foo) - assert list(result.keys()) == list(foo.keys()) == ['b', 'a'] - assert result == {'b': 2, 'a': 1} - - -@pytest.mark.parametrize('strict', [True, False]) -def test_defaultdict(strict): - v = SchemaValidator(cs.dict_schema(keys_schema=cs.str_schema(), values_schema=cs.int_schema())) - - dd = defaultdict(int, {}) - # simulate move to end, since defaultdict doesn't have it - dd['a'] = 1 - dd['b'] = 2 - dd['a'] = dd.pop('a') - - result = v.validate_python(dd, strict=strict) - assert list(result.keys()) == list(dd.keys()) == ['b', 'a'] - assert result == {'b': 2, 'a': 1} From 939940da4c6e13e9f0532cf62cf7cb3e9cf27377 Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Fri, 3 Oct 2025 17:26:04 +0200 Subject: [PATCH 19/20] update tests --- tests/validators/test_dict.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 4b332115e..a173e50c7 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -12,12 +12,6 @@ from ..conftest import Err, PyAndJson -# Skip OrderedDict tests on GraalPy due to a bug in PyMapping.items() -skip_on_graalpy = pytest.mark.skipif( - sys.implementation.name == 'graalpy', - reason='GraalPy has a bug where PyMapping.items() does not preserve OrderedDict order', -) - def test_dict(py_and_json: PyAndJson): v = py_and_json({'type': 'dict', 'keys_schema': {'type': 'int'}, 'values_schema': {'type': 'int'}}) @@ -322,7 +316,10 @@ def test_dict_fail_fast(fail_fast, expected): assert exc_info.value.errors(include_url=False) == expected -@skip_on_graalpy +@pytest.mark.skipif( + sys.implementation.name == 'graalpy', + reason='GraalPy has a bug where PyMapping.items() does not preserve OrderedDict order', +) @pytest.mark.parametrize('strict', [True, False]) def test_ordered_dict_key_order_preservation(strict): # GH 12273 From 7f8e658bdd028b8262b7a17a21bcb210bd755665 Mon Sep 17 00:00:00 2001 From: Tobias Pitters Date: Sat, 4 Oct 2025 09:55:37 +0200 Subject: [PATCH 20/20] update description for graalpy test skipping --- tests/validators/test_dict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index a173e50c7..193c56ac6 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -318,7 +318,7 @@ def test_dict_fail_fast(fail_fast, expected): @pytest.mark.skipif( sys.implementation.name == 'graalpy', - reason='GraalPy has a bug where PyMapping.items() does not preserve OrderedDict order', + reason='GraalPy has a bug where PyMapping.items() does not preserve OrderedDict order. See: https://github.com/oracle/graalpython/issues/553', ) @pytest.mark.parametrize('strict', [True, False]) def test_ordered_dict_key_order_preservation(strict):