Skip to content

Commit

Permalink
fix: allow path to be a list of strings, integers or any other hashab…
Browse files Browse the repository at this point in the history
…les (#497)
  • Loading branch information
antoni-szych-rtbhouse authored Dec 13, 2023
1 parent ca5223f commit 264b7e4
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 11 deletions.
16 changes: 11 additions & 5 deletions voluptuous/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ class Invalid(Error):
"""

def __init__(self, message: str, path: typing.Optional[typing.List[str]] = None, error_message: typing.Optional[str] = None, error_type: typing.Optional[str] = None) -> None:
def __init__(
self,
message: str,
path: typing.Optional[typing.List[typing.Hashable]] = None,
error_message: typing.Optional[str] = None,
error_type: typing.Optional[str] = None
) -> None:
Error.__init__(self, message)
self._path = path or []
self._error_message = error_message or message
Expand All @@ -30,7 +36,7 @@ def msg(self) -> str:
return self.args[0]

@property
def path(self) -> typing.List[str]:
def path(self) -> typing.List[typing.Hashable]:
return self._path

@property
Expand All @@ -45,7 +51,7 @@ def __str__(self) -> str:
output += ' for ' + self.error_type
return output + path

def prepend(self, path: typing.List[str]) -> None:
def prepend(self, path: typing.List[typing.Hashable]) -> None:
self._path = path + self.path


Expand All @@ -61,7 +67,7 @@ def msg(self) -> str:
return self.errors[0].msg

@property
def path(self) -> typing.List[str]:
def path(self) -> typing.List[typing.Hashable]:
return self.errors[0].path

@property
Expand All @@ -74,7 +80,7 @@ def add(self, error: Invalid) -> None:
def __str__(self) -> str:
return str(self.errors[0])

def prepend(self, path: typing.List[str]) -> None:
def prepend(self, path: typing.List[typing.Hashable]) -> None:
for error in self.errors:
error.prepend(path)

Expand Down
5 changes: 1 addition & 4 deletions voluptuous/humanize.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
MAX_VALIDATION_ERROR_ITEM_LENGTH = 500


IndexT = typing.TypeVar("IndexT")


def _nested_getitem(data: typing.Dict[IndexT, typing.Any], path: typing.List[IndexT]) -> typing.Optional[typing.Any]:
def _nested_getitem(data: typing.Any, path: typing.List[typing.Hashable]) -> typing.Optional[typing.Any]:
for item_index in path:
try:
data = data[item_index]
Expand Down
70 changes: 70 additions & 0 deletions voluptuous/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,76 @@ def test_match_error_has_path():
assert False, "Did not raise MatchInvalid"


def test_path_with_string():
"""Most common dict use with strings as keys"""
s = Schema({'string_key': int})

with pytest.raises(MultipleInvalid) as ctx:
s({'string_key': 'str'})
assert ctx.value.errors[0].path == ['string_key']


def test_path_with_list_index():
"""Position of the offending list index included in path as int"""
s = Schema({'string_key': [int]})

with pytest.raises(MultipleInvalid) as ctx:
s({'string_key': [123, 'should be int']})
assert ctx.value.errors[0].path == ['string_key', 1]


def test_path_with_tuple_index():
"""Position of the offending tuple index included in path as int"""
s = Schema({'string_key': (int,)})

with pytest.raises(MultipleInvalid) as ctx:
s({'string_key': (123, 'should be int')})
assert ctx.value.errors[0].path == ['string_key', 1]


def test_path_with_integer_dict_key():
"""Not obvious case with dict having not strings, but integers as keys"""
s = Schema({1337: int})

with pytest.raises(MultipleInvalid) as ctx:
s({1337: 'should be int'})
assert ctx.value.errors[0].path == [1337]


def test_path_with_float_dict_key():
"""Not obvious case with dict having not strings, but floats as keys"""
s = Schema({13.37: int})

with pytest.raises(MultipleInvalid) as ctx:
s({13.37: 'should be int'})
assert ctx.value.errors[0].path == [13.37]


def test_path_with_tuple_dict_key():
"""Not obvious case with dict having not strings, but tuples as keys"""
s = Schema({('fancy', 'key'): int})

with pytest.raises(MultipleInvalid) as ctx:
s({('fancy', 'key'): 'should be int'})
assert ctx.value.errors[0].path == [('fancy', 'key')]


def test_path_with_arbitrary_hashable_dict_key():
"""Not obvious case with dict having not strings, but arbitrary hashable objects as keys"""

class HashableObjectWhichWillBeKeyInDict:
def __hash__(self):
return 1337 # dummy hash, used only for illustration

s = Schema({HashableObjectWhichWillBeKeyInDict: [int]})

hashable_obj_provided_in_input = HashableObjectWhichWillBeKeyInDict()

with pytest.raises(MultipleInvalid) as ctx:
s({hashable_obj_provided_in_input: [0, 1, 'should be int']})
assert ctx.value.errors[0].path == [hashable_obj_provided_in_input, 2]


def test_self_any():
schema = Schema({"number": int,
"follow": Any(Self, "stop")})
Expand Down
4 changes: 2 additions & 2 deletions voluptuous/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def __voluptuous_compile__(self, schema: Schema) -> typing.Callable:
schema.required = old_required
return self._run

def _run(self, path: typing.List[str], value):
def _run(self, path: typing.List[typing.Hashable], value):
if self.discriminant is not None:
self._compiled = [
self.schema._compile(v)
Expand All @@ -243,7 +243,7 @@ def __repr__(self):
self.msg
)

def _exec(self, funcs: typing.Iterable, v, path: typing.Optional[typing.List[str]] = None):
def _exec(self, funcs: typing.Iterable, v, path: typing.Optional[typing.List[typing.Hashable]] = None):
raise NotImplementedError()


Expand Down

0 comments on commit 264b7e4

Please sign in to comment.