Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions tests/test_traitlets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1943,6 +1943,80 @@ class TestInstanceNonuniformlyValueValidatedDict(TraitTestBase):
_bad_values = [{"foo": "0", "bar": "1"}]


@pytest.mark.parametrize(
"invalid_value, expected_info",
[
(False, "a dict whose value at key 'that' is a dict"),
(
{"that": 3},
"a dict whose value at key 'that' is a dict whose value at key 'that' is a unicode string",
),
],
)
def test_dict_per_key_error(invalid_value, expected_info):
class Foo(HasTraits):
bar = Dict(
per_key_traits={"this": Unicode(), "that": Dict(per_key_traits={"that": Unicode()})}
)

foo = Foo()
with pytest.raises(TraitError) as exc:
foo.bar = {"this": "valid", "that": invalid_value}

message = str(exc.value)
assert "'bar' trait of a Foo instance" in message
assert expected_info in message


def test_dict_per_key_custom_error():
class CustomTrait(TraitType):
info_text = "a custom value"

def validate(self, obj, value):
raise TraitError("Invalid custom value")

class Foo(HasTraits):
bar = Dict(per_key_traits={"custom": CustomTrait()})

with pytest.raises(TraitError, match="a dict whose value at key 'custom' is a custom value"):
Foo().bar = {"custom": 3}


@pytest.mark.parametrize(
"value_trait, invalid_value, expected_info",
[
(List(Unicode()), [3], "a list containing a unicode string"),
(List(List(Unicode())), [[3]], "a list containing a list containing a unicode string"),
(
List(Dict(per_key_traits={"leaf": Unicode()})),
[{"leaf": 3}],
"a list containing a dict whose value at key 'leaf' is a unicode string",
),
],
)
def test_dict_per_key_container_error(value_trait, invalid_value, expected_info):
class Foo(HasTraits):
bar = Dict(per_key_traits={"items": value_trait})

with pytest.raises(TraitError) as exc:
Foo().bar = {"items": invalid_value}

assert f"a dict whose value at key 'items' is {expected_info}" in str(exc.value)


def test_dict_value_trait_error_unchanged():
class Foo(HasTraits):
bar = Dict(value_trait=Unicode(), per_key_traits={"number": Int()})

with pytest.raises(TraitError) as exc:
Foo().bar = {"number": 1, "string": 3}

assert str(exc.value) == (
"Values of the 'bar' trait of a Foo instance must be a unicode string, "
"but a value of 3 <class 'int'> was specified."
)


class KeyValidatedDictTrait(HasTraits):
value = Dict(key_trait=Unicode(), default_value={"foo": "1"})

Expand Down
8 changes: 7 additions & 1 deletion traitlets/traitlets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4057,7 +4057,13 @@ def validate_elements(self, obj: t.Any, value: dict[t.Any, t.Any]) -> dict[K, V]
if active_value_trait:
try:
v = active_value_trait._validate(obj, v)
except TraitError:
except TraitError as error:
if key in per_key_override:
info = error.args[1] if len(error.args) > 1 else active_value_trait.info()
# Nested traits append their containers to the error chain.
for container in error.args[3:]:
info = f"{container.info()} containing {info}"
self.error(obj, value, info=f"a dict whose value at key {key!r} is {info}")
self.element_error(obj, v, active_value_trait, "Values")
validated[key] = v

Expand Down
Loading