77#
88"""Tests for annotated types in deeplabcut.core.config.validation."""
99
10+ from typing import Any
11+
1012import numpy as np
1113import pytest
12- from pydantic import ValidationError
14+ from pydantic import ValidationError , field_validator
1315
1416from deeplabcut .core .config import DLCBaseConfig
1517from deeplabcut .core .config .validation import (
2022 UniqueStrList ,
2123 greater_than ,
2224 less_than ,
25+ reject_reserved_keys ,
2326 unique_values ,
2427 validate_crop_bounds ,
2528)
@@ -40,6 +43,19 @@ class ListConfig(DLCBaseConfig):
4043 indices : NDArrayInt = None
4144
4245
46+ class KwargsConfig (DLCBaseConfig ):
47+ """Config exercising a pass-through kwargs field with reserved keys."""
48+
49+ name : str = "run"
50+ call_kwargs : dict [str , Any ] | None = None
51+
52+ @field_validator ("call_kwargs" )
53+ @classmethod
54+ def validate_call_kwargs (cls , v : dict [str , Any ] | None ) -> dict [str , Any ] | None :
55+ reject_reserved_keys (v , {"name" : "name" , "dataset" : None }, name = "call_kwargs" )
56+ return v
57+
58+
4359# ------------------------------------------------------------------
4460# DefaultIfNone
4561# ------------------------------------------------------------------
@@ -215,3 +231,42 @@ def test_existing_array_is_passed_through_unchanged(self):
215231 indices = ListConfig (indices = array ).indices
216232 assert indices is array
217233 assert indices .dtype == np .float64
234+
235+
236+ # ------------------------------------------------------------------
237+ # reject_reserved_keys
238+ # ------------------------------------------------------------------
239+
240+
241+ class TestValidateNoReservedKeys :
242+ @pytest .mark .parametrize ("value" , [None , {}, {"mode" : "offline" , "tags" : ["a" ]}], ids = ["none" , "empty" , "free" ])
243+ def test_accepts_unreserved_kwargs (self , value ):
244+ assert KwargsConfig (call_kwargs = value ).call_kwargs == value
245+
246+ def test_rejects_reserved_key_with_field_hint (self ):
247+ with pytest .raises (
248+ ValidationError , match = r"call_kwargs cannot include reserved options: 'name' \(set via `name`\)"
249+ ):
250+ KwargsConfig (call_kwargs = {"name" : "other" })
251+
252+ def test_rejects_reserved_key_even_when_value_matches_field (self ):
253+ with pytest .raises (ValidationError , match = "reserved options" ):
254+ KwargsConfig (name = "run" , call_kwargs = {"name" : "run" })
255+
256+ def test_rejects_reserved_key_without_config_field (self ):
257+ with pytest .raises (ValidationError , match = "reserved options: 'dataset'" ) as excinfo :
258+ KwargsConfig (call_kwargs = {"dataset" : "x" })
259+ assert "set via" not in str (excinfo .value )
260+
261+ def test_rejects_reserved_key_on_assignment (self ):
262+ cfg = KwargsConfig ()
263+ with pytest .raises (ValidationError , match = "reserved options" ):
264+ cfg .call_kwargs = {"dataset" : "x" }
265+
266+ @pytest .mark .parametrize ("kwargs" , [None , {}, {"mode" : "offline" }], ids = ["none" , "empty" , "free" ])
267+ def test_function_accepts_unreserved_kwargs (self , kwargs ):
268+ reject_reserved_keys (kwargs , {"dataset" : None }, name = "kw" )
269+
270+ def test_function_lists_all_clashes_sorted (self ):
271+ with pytest .raises (ValueError , match = r"kw cannot include reserved options: 'a' \(set via `x`\), 'b'$" ):
272+ reject_reserved_keys ({"b" : 1 , "a" : 2 , "c" : 3 }, {"a" : "x" , "b" : None }, name = "kw" )
0 commit comments