Skip to content

Commit fa6daf5

Browse files
authored
Merge pull request #774 from krassowski/improve-union-parsing
2 parents 367ac77 + e76aa18 commit fa6daf5

2 files changed

Lines changed: 34 additions & 3 deletions

File tree

traitlets/tests/test_traitlets.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ def notify_change(self, change):
8282
self._notify_type = change["type"]
8383

8484

85+
class CrossValidationStub(HasTraits):
86+
_cross_validation_lock = False
87+
88+
8589
# -----------------------------------------------------------------------------
8690
# Test classes
8791
# -----------------------------------------------------------------------------
@@ -2924,7 +2928,7 @@ def _from_string_test(traittype, s, expected):
29242928
if type(expected) is type and issubclass(expected, Exception):
29252929
with pytest.raises(expected):
29262930
value = cast(s)
2927-
trait.validate(None, value)
2931+
trait.validate(CrossValidationStub(), value)
29282932
else:
29292933
value = cast(s)
29302934
assert value == expected
@@ -3144,6 +3148,22 @@ def test_union_of_list_and_unicode_from_string(s, expected):
31443148
_from_string_test(Union([List(), Unicode()]), s, expected)
31453149

31463150

3151+
@pytest.mark.parametrize(
3152+
"s, expected",
3153+
[("1", 1), ("1.5", 1.5)],
3154+
)
3155+
def test_union_of_int_and_float_from_string(s, expected):
3156+
_from_string_test(Union([Int(), Float()]), s, expected)
3157+
3158+
3159+
@pytest.mark.parametrize(
3160+
"s, expected, allow_none",
3161+
[("[]", [], False), ("{}", {}, False), ("None", TraitError, False), ("None", None, True)],
3162+
)
3163+
def test_union_of_list_and_dict_from_string(s, expected, allow_none):
3164+
_from_string_test(Union([List(), Dict()], allow_none=allow_none), s, expected)
3165+
3166+
31473167
def test_all_attribute():
31483168
"""Verify all trait types are added to `traitlets.__all__`"""
31493169
names = dir(traitlets)

traitlets/traitlets.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2131,11 +2131,22 @@ def __init__(self, trait_types, **kwargs):
21312131
----------
21322132
trait_types : sequence
21332133
The list of trait types of length at least 1.
2134+
**kwargs
2135+
Extra kwargs passed to `TraitType`
21342136
21352137
Notes
21362138
-----
21372139
Union([Float(), Bool(), Int()]) attempts to validate the provided values
21382140
with the validation function of Float, then Bool, and finally Int.
2141+
2142+
Parsing from string is ambiguous for container types which accept other
2143+
collection-like literals (e.g. List accepting both `[]` and `()`
2144+
precludes Union from ever parsing ``Union([List(), Tuple()])`` as a tuple;
2145+
you can modify behaviour of too permissive container traits by overriding
2146+
``_literal_from_string_pairs`` in subclasses.
2147+
Similarly, parsing unions of numeric types is only unambiguous if
2148+
types are provided in order of increasing permissiveness, e.g.
2149+
``Union([Int(), Float()])`` (since floats accept integer-looking values).
21392150
"""
21402151
self.trait_types = list(trait_types)
21412152
self.info_text = " or ".join([tt.info() for tt in self.trait_types])
@@ -2184,9 +2195,9 @@ def from_string(self, s):
21842195
try:
21852196
v = trait_type.from_string(s)
21862197
return trait_type.validate(None, v)
2187-
except TraitError:
2198+
except (TraitError, ValueError):
21882199
continue
2189-
self.error(None, s)
2200+
return super().from_string(s)
21902201

21912202

21922203
# -----------------------------------------------------------------------------

0 commit comments

Comments
 (0)