back to Claude Sonnet 3.5 - Fill-in + Unit Test Feedback summary
Claude Sonnet 3.5 - Fill-in + Unit Test Feedback: voluptuous
Failed to run pytests for test tests
Pytest collection failure.
Patch diff
diff --git a/voluptuous/error.py b/voluptuous/error.py
index f72fbe7..41fe0c6 100644
--- a/voluptuous/error.py
+++ b/voluptuous/error.py
@@ -17,6 +17,36 @@ class Invalid(Error):
:attr error_message: The actual error message that was raised, as a
string.
+def raises(exc, msg=None, regex=None):
+ def raises(exc, msg=None, regex=None):
+ """Return a context manager that raises an exception.
+
+ This is useful for testing that certain errors are raised.
+
+ :param exc: The exception class to raise.
+ :param msg: Optional message to check against the exception.
+ :param regex: Optional regular expression to match against the exception message.
+ """
+
+ This is useful for testing that certain errors are raised.
+
+ :param exc: The exception class to raise.
+ :param msg: Optional message to check against the exception.
+ :param regex: Optional regular expression to match against the exception message.
+ """
+ @contextmanager
+ def context():
+ try:
+ yield
+ except exc as e:
+ if msg is not None and str(e) != msg:
+ raise AssertionError(f"Exception message '{str(e)}' does not match '{msg}'")
+ if regex is not None and not re.search(regex, str(e)):
+ raise AssertionError(f"Exception message '{str(e)}' does not match regex '{regex}'")
+ else:
+ raise AssertionError(f"{exc.__name__} not raised")
+ return context
+
"""
def __init__(self, message: str, path: typing.Optional[typing.List[
@@ -146,7 +176,7 @@ class LiteralInvalid(Invalid):
class LengthInvalid(Invalid):
- pass
+ """The value has an invalid length."""
class DatetimeInvalid(Invalid):
@@ -158,22 +188,41 @@ class DateInvalid(Invalid):
class InInvalid(Invalid):
- pass
+ """The value is not in the required collection."""
class NotInInvalid(Invalid):
- pass
+ """The value is in a collection it should not be in."""
class ExactSequenceInvalid(Invalid):
- pass
+ """The sequence does not match exactly."""
class NotEnoughValid(Invalid):
"""The value did not pass enough validations."""
- pass
+ def __init__(self, msg: str, path: typing.Optional[typing.List[typing.Hashable]] = None,
+ error_message: typing.Optional[str] = None,
+ error_type: typing.Optional[str] = None,
+ expected: int = 0, actual: int = 0) -> None:
+ super().__init__(msg, path, error_message, error_type)
+ self.expected = expected
+ self.actual = actual
+
+ def __str__(self) -> str:
+ return f"{super().__str__()} (expected at least {self.expected}, got {self.actual})"
class TooManyValid(Invalid):
"""The value passed more than expected validations."""
- pass
+
+ def __init__(self, msg: str, path: typing.Optional[typing.List[typing.Hashable]] = None,
+ error_message: typing.Optional[str] = None,
+ error_type: typing.Optional[str] = None,
+ expected: int = 0, actual: int = 0) -> None:
+ super().__init__(msg, path, error_message, error_type)
+ self.expected = expected
+ self.actual = actual
+
+ def __str__(self) -> str:
+ return f"{super().__str__()} (expected at most {self.expected}, got {self.actual})"
diff --git a/voluptuous/humanize.py b/voluptuous/humanize.py
index 2902871..fabb7d0 100644
--- a/voluptuous/humanize.py
+++ b/voluptuous/humanize.py
@@ -11,4 +11,29 @@ def humanize_error(data, validation_error: Invalid, max_sub_error_length:
Invalid and MultipleInvalid do not include the offending value in error messages,
and MultipleInvalid.__str__ only provides the first error.
"""
- pass
+ if isinstance(validation_error, MultipleInvalid):
+ errors = validation_error.errors
+ else:
+ errors = [validation_error]
+
+ error_messages = []
+ for error in errors:
+ path = ' -> '.join(str(p) for p in error.path) if error.path else 'root'
+ value = get_value_from_path(data, error.path)
+ value_str = str(value)[:max_sub_error_length]
+ if len(str(value)) > max_sub_error_length:
+ value_str += '...'
+ error_messages.append(f"Error at {path}: {error.error_message}. Got: {value_str}")
+
+ return '\n'.join(error_messages)
+
+def get_value_from_path(data, path):
+ """Helper function to get the value from the data using the error path."""
+ for key in path:
+ if isinstance(data, (list, tuple)) and isinstance(key, int):
+ data = data[key] if 0 <= key < len(data) else None
+ elif isinstance(data, dict):
+ data = data.get(key)
+ else:
+ return None
+ return data
diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py
index de2b53c..df86a0a 100644
--- a/voluptuous/schema_builder.py
+++ b/voluptuous/schema_builder.py
@@ -113,7 +113,21 @@ class Schema(object):
Note: only very basic inference is supported.
"""
- pass
+ def infer_type(value):
+ if isinstance(value, dict):
+ return {k: infer_type(v) for k, v in value.items()}
+ elif isinstance(value, list):
+ if value:
+ return [infer_type(value[0])]
+ else:
+ return list
+ elif isinstance(value, (int, float, str, bool)):
+ return type(value)
+ else:
+ return object
+
+ inferred_schema = infer_type(data)
+ return cls(inferred_schema, **kwargs)
def __eq__(self, other):
if not isinstance(other, Schema):
@@ -142,7 +156,34 @@ class Schema(object):
def _compile_mapping(self, schema, invalid_msg=None):
"""Create validator for given mapping."""
- pass
+ def validate_mapping(path, data):
+ if not isinstance(data, dict):
+ raise er.Invalid(invalid_msg or 'expected a dictionary', path)
+
+ out = type(data)()
+ for key, value in data.items():
+ key_path = path + [key]
+ if key in schema:
+ out[key] = schema[key](key_path, value)
+ elif isinstance(key, str) and key.startswith('$'):
+ out[key] = value
+ elif Any in schema:
+ out[key] = schema[Any](key_path, value)
+ elif self.extra == ALLOW_EXTRA:
+ out[key] = value
+ elif self.extra != REMOVE_EXTRA:
+ raise er.Invalid('extra keys not allowed', key_path)
+
+ for key, validator in schema.items():
+ if isinstance(key, Required) and key.schema not in out:
+ if key.default is not UNDEFINED:
+ out[key.schema] = key.default()
+ else:
+ raise er.RequiredFieldInvalid('required key not provided', path + [key.schema])
+
+ return out
+
+ return validate_mapping
def _compile_object(self, schema):
"""Validate an object.
@@ -162,7 +203,27 @@ class Schema(object):
... validate(Structure(one='three'))
"""
- pass
+ def validate_object(path, data):
+ if not isinstance(data, schema.cls):
+ raise er.Invalid(f'expected {schema.cls.__name__}', path)
+
+ out = schema.cls()
+ for key, validator in schema.items():
+ if hasattr(data, key):
+ value = getattr(data, key)
+ try:
+ setattr(out, key, validator(path + [key], value))
+ except er.Invalid as e:
+ raise er.Invalid(f'not a valid value for object value', path + [key]) from e
+ elif isinstance(key, Required):
+ if key.default is not UNDEFINED:
+ setattr(out, key.schema, key.default())
+ else:
+ raise er.RequiredFieldInvalid('required attribute not provided', path + [key.schema])
+
+ return out
+
+ return validate_object
def _compile_dict(self, schema):
"""Validate a dictionary.
@@ -240,7 +301,48 @@ class Schema(object):
"expected str for dictionary value @ data['adict']['strfield']"]
"""
- pass
+ def validate_dict(path, data):
+ if not isinstance(data, dict):
+ raise er.Invalid('expected a dictionary', path)
+
+ out = type(data)()
+ errors = []
+ for key, value in data.items():
+ key_path = path + [key]
+ if key in schema:
+ try:
+ out[key] = schema[key](key_path, value)
+ except er.Invalid as e:
+ errors.append(e)
+ elif isinstance(key, type):
+ if any(isinstance(k, type) for k in schema):
+ continue
+ try:
+ valid_key = next(k for k in schema if isinstance(k, type) and isinstance(key, k))
+ out[key] = schema[valid_key](key_path, value)
+ except StopIteration:
+ if self.extra == ALLOW_EXTRA:
+ out[key] = value
+ elif self.extra != REMOVE_EXTRA:
+ errors.append(er.Invalid('extra keys not allowed', key_path))
+ elif self.extra == ALLOW_EXTRA:
+ out[key] = value
+ elif self.extra != REMOVE_EXTRA:
+ errors.append(er.Invalid('extra keys not allowed', key_path))
+
+ for key in schema:
+ if isinstance(key, Required) and key not in out:
+ if key.default is not UNDEFINED:
+ out[key.schema] = key.default()
+ else:
+ errors.append(er.RequiredFieldInvalid('required key not provided', path + [key.schema]))
+
+ if errors:
+ raise er.MultipleInvalid(errors)
+
+ return out
+
+ return validate_dict
def _compile_sequence(self, schema, seq_type):
"""Validate a sequence type.
@@ -255,7 +357,28 @@ class Schema(object):
>>> validator([1])
[1]
"""
- pass
+ def validate_sequence(path, data):
+ if not isinstance(data, seq_type):
+ raise er.Invalid(f'expected a {seq_type.__name__}', path)
+
+ out = []
+ errors = []
+ for i, value in enumerate(data):
+ item_path = path + [i]
+ for validator in schema:
+ try:
+ out.append(validator(item_path, value))
+ break
+ except er.Invalid as e:
+ if validator == schema[-1]:
+ errors.append(e)
+
+ if errors:
+ raise er.MultipleInvalid(errors)
+
+ return seq_type(out)
+
+ return validate_sequence
def _compile_tuple(self, schema):
"""Validate a tuple.
@@ -270,7 +393,7 @@ class Schema(object):
>>> validator((1,))
(1,)
"""
- pass
+ return self._compile_sequence(schema, tuple)
def _compile_list(self, schema):
"""Validate a list.
@@ -285,7 +408,7 @@ class Schema(object):
>>> validator([1])
[1]
"""
- pass
+ return self._compile_sequence(schema, list)
def _compile_set(self, schema):
"""Validate a set.
@@ -300,7 +423,28 @@ class Schema(object):
>>> with raises(er.MultipleInvalid, 'invalid value in set'):
... validator(set(['a']))
"""
- pass
+ def validate_set(path, data):
+ if not isinstance(data, set):
+ raise er.Invalid('expected a set', path)
+
+ out = set()
+ errors = []
+ for i, value in enumerate(data):
+ item_path = path + [i]
+ for validator in schema:
+ try:
+ out.add(validator(item_path, value))
+ break
+ except er.Invalid as e:
+ if validator == schema[-1]:
+ errors.append(e)
+
+ if errors:
+ raise er.MultipleInvalid(errors)
+
+ return out
+
+ return validate_set
def extend(self, schema: Schemable, required: typing.Optional[bool]=
None, extra: typing.Optional[int]=None) ->Schema:
@@ -316,7 +460,17 @@ class Schema(object):
:param required: if set, overrides `required` of this `Schema`
:param extra: if set, overrides `extra` of this `Schema`
"""
- pass
+ if not isinstance(self.schema, dict) or not isinstance(schema, dict):
+ raise ValueError("Both schemas must be dictionary-based")
+
+ new_schema = self.schema.copy()
+ new_schema.update(schema)
+
+ return Schema(
+ new_schema,
+ required=self.required if required is None else required,
+ extra=self.extra if extra is None else extra
+ )
def _compile_scalar(schema):
@@ -707,4 +861,16 @@ def validate(*a, **kw) ->typing.Callable:
... return arg1 * 2
"""
- pass
+ def decorator(func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ schema = Schema(dict(zip(func.__code__.co_varnames, a)), **kw)
+ bound_args = inspect.signature(func).bind(*args, **kwargs)
+ bound_args.apply_defaults()
+ validated_args = schema(dict(bound_args.arguments))
+ result = func(**validated_args)
+ if '__return__' in kw:
+ return Schema(kw['__return__'])(result)
+ return result
+ return wrapper
+ return decorator
diff --git a/voluptuous/util.py b/voluptuous/util.py
index fe15b1a..a2c57d8 100644
--- a/voluptuous/util.py
+++ b/voluptuous/util.py
@@ -13,7 +13,7 @@ def Lower(v: str) ->str:
>>> s('HI')
'hi'
"""
- pass
+ return v.lower()
def Upper(v: str) ->str:
@@ -23,7 +23,7 @@ def Upper(v: str) ->str:
>>> s('hi')
'HI'
"""
- pass
+ return v.upper()
def Capitalize(v: str) ->str:
@@ -33,7 +33,7 @@ def Capitalize(v: str) ->str:
>>> s('hello world')
'Hello world'
"""
- pass
+ return v.capitalize()
def Title(v: str) ->str:
@@ -43,7 +43,7 @@ def Title(v: str) ->str:
>>> s('hello world')
'Hello World'
"""
- pass
+ return v.title()
def Strip(v: str) ->str:
@@ -53,7 +53,7 @@ def Strip(v: str) ->str:
>>> s(' hello world ')
'hello world'
"""
- pass
+ return v.strip()
class DefaultTo(object):
diff --git a/voluptuous/validators.py b/voluptuous/validators.py
index 88b50f6..5918e98 100644
--- a/voluptuous/validators.py
+++ b/voluptuous/validators.py
@@ -7,7 +7,8 @@ import typing
from decimal import Decimal, InvalidOperation
from functools import wraps
from voluptuous.error import AllInvalid, AnyInvalid, BooleanInvalid, CoerceInvalid, ContainsInvalid, DateInvalid, DatetimeInvalid, DirInvalid, EmailInvalid, ExactSequenceInvalid, FalseInvalid, FileInvalid, InInvalid, Invalid, LengthInvalid, MatchInvalid, MultipleInvalid, NotEnoughValid, NotInInvalid, PathInvalid, RangeInvalid, TooManyValid, TrueInvalid, TypeInvalid, UrlInvalid
-from voluptuous.schema_builder import Schema, Schemable, message, raises
+from voluptuous.schema_builder import Schema, Schemable, message
+from voluptuous.error import raises
if typing.TYPE_CHECKING:
from _typeshed import SupportsAllComparisons
Enum: typing.Union[type, None]
@@ -41,7 +42,13 @@ def truth(f: typing.Callable) ->typing.Callable:
>>> with raises(MultipleInvalid, 'not a valid value'):
... validate('/notavaliddir')
"""
- pass
+ @wraps(f)
+ def wrapper(v):
+ t = f(v)
+ if not t:
+ raise ValueError
+ return v
+ return wrapper
class Coerce(object):
@@ -109,7 +116,7 @@ def IsTrue(v):
... except MultipleInvalid as e:
... assert isinstance(e.errors[0], TrueInvalid)
"""
- pass
+ return bool(v)
@message('value was not false', cls=FalseInvalid)
@@ -129,7 +136,9 @@ def IsFalse(v):
... except MultipleInvalid as e:
... assert isinstance(e.errors[0], FalseInvalid)
"""
- pass
+ if bool(v):
+ raise ValueError
+ return v
@message('expected boolean', cls=BooleanInvalid)
@@ -153,7 +162,18 @@ def Boolean(v):
... except MultipleInvalid as e:
... assert isinstance(e.errors[0], BooleanInvalid)
"""
- pass
+ if isinstance(v, bool):
+ return v
+ if isinstance(v, str):
+ v = v.lower()
+ if v in ('1', 'true', 'yes', 'on', 'enable'):
+ return True
+ if v in ('0', 'false', 'no', 'off', 'disable'):
+ return False
+ try:
+ return bool(v)
+ except ValueError:
+ raise BooleanInvalid('expected boolean')
class _WithSubValidators(object):
@@ -213,6 +233,15 @@ class Any(_WithSubValidators):
>>> with raises(MultipleInvalid, "Expected 1 2 or 3"):
... validate(4)
"""
+ def _run(self, v):
+ errors = []
+ for validator in self._compiled:
+ try:
+ return validator(v)
+ except Invalid as e:
+ errors.append(e)
+ else:
+ raise AnyInvalid(self.msg or errors)
Or = Any
@@ -255,6 +284,13 @@ class All(_WithSubValidators):
>>> validate('10')
10
"""
+ def _run(self, v):
+ try:
+ for validator in self._compiled:
+ v = validator(v)
+ except Invalid as e:
+ raise AllInvalid(self.msg or str(e))
+ return v
And = All
@@ -340,7 +376,17 @@ def Email(v):
>>> s('t@x.com')
't@x.com'
"""
- pass
+ try:
+ if not isinstance(v, str):
+ raise ValueError
+ user_part, domain_part = v.rsplit('@', 1)
+ if not USER_REGEX.match(user_part):
+ raise ValueError
+ if not DOMAIN_REGEX.match(domain_part):
+ raise ValueError
+ except ValueError:
+ raise EmailInvalid('expected an email address')
+ return v
@message('expected a fully qualified domain name URL', cls=UrlInvalid)
@@ -353,7 +399,17 @@ def FqdnUrl(v):
>>> s('http://w3.org')
'http://w3.org'
"""
- pass
+ try:
+ if not isinstance(v, str):
+ raise ValueError
+ parsed = urlparse.urlparse(v)
+ if not parsed.scheme or not parsed.netloc or parsed.netloc == 'localhost':
+ raise ValueError
+ if not DOMAIN_REGEX.match(parsed.netloc):
+ raise ValueError
+ except ValueError:
+ raise UrlInvalid('expected a fully qualified domain name URL')
+ return v
@message('expected a URL', cls=UrlInvalid)
@@ -366,7 +422,15 @@ def Url(v):
>>> s('http://w3.org')
'http://w3.org'
"""
- pass
+ try:
+ if not isinstance(v, str):
+ raise ValueError
+ parsed = urlparse.urlparse(v)
+ if not parsed.scheme or not parsed.netloc:
+ raise ValueError
+ except ValueError:
+ raise UrlInvalid('expected a URL')
+ return v
@message('Not a file', cls=FileInvalid)
@@ -425,7 +489,11 @@ def Maybe(validator: Schemable, msg: typing.Optional[str]=None):
... s("string")
"""
- pass
+ def maybe_validator(v):
+ if v is None:
+ return v
+ return Schema(validator)(v)
+ return maybe_validator
class Range(object):