add tclint
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""Schema validation for Python data structures.
|
||||
|
||||
Given eg. a nested data structure like this:
|
||||
|
||||
{
|
||||
'exclude': ['Users', 'Uptime'],
|
||||
'include': [],
|
||||
'set': {
|
||||
'snmp_community': 'public',
|
||||
'snmp_timeout': 15,
|
||||
'snmp_version': '2c',
|
||||
},
|
||||
'targets': {
|
||||
'localhost': {
|
||||
'exclude': ['Uptime'],
|
||||
'features': {
|
||||
'Uptime': {
|
||||
'retries': 3,
|
||||
},
|
||||
'Users': {
|
||||
'snmp_community': 'monkey',
|
||||
'snmp_port': 15,
|
||||
},
|
||||
},
|
||||
'include': ['Users'],
|
||||
'set': {
|
||||
'snmp_community': 'monkeys',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
A schema like this:
|
||||
|
||||
>>> settings = {
|
||||
... 'snmp_community': str,
|
||||
... 'retries': int,
|
||||
... 'snmp_version': All(Coerce(str), Any('3', '2c', '1')),
|
||||
... }
|
||||
>>> features = ['Ping', 'Uptime', 'Http']
|
||||
>>> schema = Schema({
|
||||
... 'exclude': features,
|
||||
... 'include': features,
|
||||
... 'set': settings,
|
||||
... 'targets': {
|
||||
... 'exclude': features,
|
||||
... 'include': features,
|
||||
... 'features': {
|
||||
... str: settings,
|
||||
... },
|
||||
... },
|
||||
... })
|
||||
|
||||
Validate like so:
|
||||
|
||||
>>> schema({
|
||||
... 'set': {
|
||||
... 'snmp_community': 'public',
|
||||
... 'snmp_version': '2c',
|
||||
... },
|
||||
... 'targets': {
|
||||
... 'exclude': ['Ping'],
|
||||
... 'features': {
|
||||
... 'Uptime': {'retries': 3},
|
||||
... 'Users': {'snmp_community': 'monkey'},
|
||||
... },
|
||||
... },
|
||||
... }) == {
|
||||
... 'set': {'snmp_version': '2c', 'snmp_community': 'public'},
|
||||
... 'targets': {
|
||||
... 'exclude': ['Ping'],
|
||||
... 'features': {'Uptime': {'retries': 3},
|
||||
... 'Users': {'snmp_community': 'monkey'}}}}
|
||||
True
|
||||
"""
|
||||
|
||||
# flake8: noqa
|
||||
# fmt: off
|
||||
from voluptuous.schema_builder import *
|
||||
from voluptuous.util import *
|
||||
from voluptuous.validators import *
|
||||
|
||||
from voluptuous.error import * # isort: skip
|
||||
|
||||
# fmt: on
|
||||
|
||||
__version__ = '0.15.2'
|
||||
__author__ = 'alecthomas'
|
||||
@@ -0,0 +1,219 @@
|
||||
# fmt: off
|
||||
import typing
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base validation exception."""
|
||||
|
||||
|
||||
class SchemaError(Error):
|
||||
"""An error was encountered in the schema."""
|
||||
|
||||
|
||||
class Invalid(Error):
|
||||
"""The data was invalid.
|
||||
|
||||
:attr msg: The error message.
|
||||
:attr path: The path to the error, as a list of keys in the source data.
|
||||
:attr error_message: The actual error message that was raised, as a
|
||||
string.
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
self.error_type = error_type
|
||||
|
||||
@property
|
||||
def msg(self) -> str:
|
||||
return self.args[0]
|
||||
|
||||
@property
|
||||
def path(self) -> typing.List[typing.Hashable]:
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def error_message(self) -> str:
|
||||
return self._error_message
|
||||
|
||||
def __str__(self) -> str:
|
||||
path = ' @ data[%s]' % ']['.join(map(repr, self.path)) if self.path else ''
|
||||
output = Exception.__str__(self)
|
||||
if self.error_type:
|
||||
output += ' for ' + self.error_type
|
||||
return output + path
|
||||
|
||||
def prepend(self, path: typing.List[typing.Hashable]) -> None:
|
||||
self._path = path + self.path
|
||||
|
||||
|
||||
class MultipleInvalid(Invalid):
|
||||
def __init__(self, errors: typing.Optional[typing.List[Invalid]] = None) -> None:
|
||||
self.errors = errors[:] if errors else []
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'MultipleInvalid(%r)' % self.errors
|
||||
|
||||
@property
|
||||
def msg(self) -> str:
|
||||
return self.errors[0].msg
|
||||
|
||||
@property
|
||||
def path(self) -> typing.List[typing.Hashable]:
|
||||
return self.errors[0].path
|
||||
|
||||
@property
|
||||
def error_message(self) -> str:
|
||||
return self.errors[0].error_message
|
||||
|
||||
def add(self, error: Invalid) -> None:
|
||||
self.errors.append(error)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.errors[0])
|
||||
|
||||
def prepend(self, path: typing.List[typing.Hashable]) -> None:
|
||||
for error in self.errors:
|
||||
error.prepend(path)
|
||||
|
||||
|
||||
class RequiredFieldInvalid(Invalid):
|
||||
"""Required field was missing."""
|
||||
|
||||
|
||||
class ObjectInvalid(Invalid):
|
||||
"""The value we found was not an object."""
|
||||
|
||||
|
||||
class DictInvalid(Invalid):
|
||||
"""The value found was not a dict."""
|
||||
|
||||
|
||||
class ExclusiveInvalid(Invalid):
|
||||
"""More than one value found in exclusion group."""
|
||||
|
||||
|
||||
class InclusiveInvalid(Invalid):
|
||||
"""Not all values found in inclusion group."""
|
||||
|
||||
|
||||
class SequenceTypeInvalid(Invalid):
|
||||
"""The type found is not a sequence type."""
|
||||
|
||||
|
||||
class TypeInvalid(Invalid):
|
||||
"""The value was not of required type."""
|
||||
|
||||
|
||||
class ValueInvalid(Invalid):
|
||||
"""The value was found invalid by evaluation function."""
|
||||
|
||||
|
||||
class ContainsInvalid(Invalid):
|
||||
"""List does not contain item"""
|
||||
|
||||
|
||||
class ScalarInvalid(Invalid):
|
||||
"""Scalars did not match."""
|
||||
|
||||
|
||||
class CoerceInvalid(Invalid):
|
||||
"""Impossible to coerce value to type."""
|
||||
|
||||
|
||||
class AnyInvalid(Invalid):
|
||||
"""The value did not pass any validator."""
|
||||
|
||||
|
||||
class AllInvalid(Invalid):
|
||||
"""The value did not pass all validators."""
|
||||
|
||||
|
||||
class MatchInvalid(Invalid):
|
||||
"""The value does not match the given regular expression."""
|
||||
|
||||
|
||||
class RangeInvalid(Invalid):
|
||||
"""The value is not in given range."""
|
||||
|
||||
|
||||
class TrueInvalid(Invalid):
|
||||
"""The value is not True."""
|
||||
|
||||
|
||||
class FalseInvalid(Invalid):
|
||||
"""The value is not False."""
|
||||
|
||||
|
||||
class BooleanInvalid(Invalid):
|
||||
"""The value is not a boolean."""
|
||||
|
||||
|
||||
class UrlInvalid(Invalid):
|
||||
"""The value is not a URL."""
|
||||
|
||||
|
||||
class EmailInvalid(Invalid):
|
||||
"""The value is not an email address."""
|
||||
|
||||
|
||||
class FileInvalid(Invalid):
|
||||
"""The value is not a file."""
|
||||
|
||||
|
||||
class DirInvalid(Invalid):
|
||||
"""The value is not a directory."""
|
||||
|
||||
|
||||
class PathInvalid(Invalid):
|
||||
"""The value is not a path."""
|
||||
|
||||
|
||||
class LiteralInvalid(Invalid):
|
||||
"""The literal values do not match."""
|
||||
|
||||
|
||||
class LengthInvalid(Invalid):
|
||||
pass
|
||||
|
||||
|
||||
class DatetimeInvalid(Invalid):
|
||||
"""The value is not a formatted datetime string."""
|
||||
|
||||
|
||||
class DateInvalid(Invalid):
|
||||
"""The value is not a formatted date string."""
|
||||
|
||||
|
||||
class InInvalid(Invalid):
|
||||
pass
|
||||
|
||||
|
||||
class NotInInvalid(Invalid):
|
||||
pass
|
||||
|
||||
|
||||
class ExactSequenceInvalid(Invalid):
|
||||
pass
|
||||
|
||||
|
||||
class NotEnoughValid(Invalid):
|
||||
"""The value did not pass enough validations."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TooManyValid(Invalid):
|
||||
"""The value passed more than expected validations."""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,57 @@
|
||||
# fmt: off
|
||||
import typing
|
||||
|
||||
from voluptuous import Invalid, MultipleInvalid
|
||||
from voluptuous.error import Error
|
||||
from voluptuous.schema_builder import Schema
|
||||
|
||||
# fmt: on
|
||||
|
||||
MAX_VALIDATION_ERROR_ITEM_LENGTH = 500
|
||||
|
||||
|
||||
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]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
# The index is not present in the dictionary, list or other
|
||||
# indexable or data is not subscriptable
|
||||
return None
|
||||
return data
|
||||
|
||||
|
||||
def humanize_error(
|
||||
data,
|
||||
validation_error: Invalid,
|
||||
max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH,
|
||||
) -> str:
|
||||
"""Provide a more helpful + complete validation error message than that provided automatically
|
||||
Invalid and MultipleInvalid do not include the offending value in error messages,
|
||||
and MultipleInvalid.__str__ only provides the first error.
|
||||
"""
|
||||
if isinstance(validation_error, MultipleInvalid):
|
||||
return '\n'.join(
|
||||
sorted(
|
||||
humanize_error(data, sub_error, max_sub_error_length)
|
||||
for sub_error in validation_error.errors
|
||||
)
|
||||
)
|
||||
else:
|
||||
offending_item_summary = repr(_nested_getitem(data, validation_error.path))
|
||||
if len(offending_item_summary) > max_sub_error_length:
|
||||
offending_item_summary = (
|
||||
offending_item_summary[: max_sub_error_length - 3] + '...'
|
||||
)
|
||||
return '%s. Got %s' % (validation_error, offending_item_summary)
|
||||
|
||||
|
||||
def validate_with_humanized_errors(
|
||||
data, schema: Schema, max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH
|
||||
) -> typing.Any:
|
||||
try:
|
||||
return schema(data)
|
||||
except (Invalid, MultipleInvalid) as e:
|
||||
raise Error(humanize_error(data, e, max_sub_error_length))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
# F401: "imported but unused"
|
||||
# fmt: off
|
||||
import typing
|
||||
|
||||
from voluptuous import validators # noqa: F401
|
||||
from voluptuous.error import Invalid, LiteralInvalid, TypeInvalid # noqa: F401
|
||||
from voluptuous.schema_builder import DefaultFactory # noqa: F401
|
||||
from voluptuous.schema_builder import Schema, default_factory, raises # noqa: F401
|
||||
|
||||
# fmt: on
|
||||
|
||||
__author__ = 'tusharmakkar08'
|
||||
|
||||
|
||||
def Lower(v: str) -> str:
|
||||
"""Transform a string to lower case.
|
||||
|
||||
>>> s = Schema(Lower)
|
||||
>>> s('HI')
|
||||
'hi'
|
||||
"""
|
||||
return str(v).lower()
|
||||
|
||||
|
||||
def Upper(v: str) -> str:
|
||||
"""Transform a string to upper case.
|
||||
|
||||
>>> s = Schema(Upper)
|
||||
>>> s('hi')
|
||||
'HI'
|
||||
"""
|
||||
return str(v).upper()
|
||||
|
||||
|
||||
def Capitalize(v: str) -> str:
|
||||
"""Capitalise a string.
|
||||
|
||||
>>> s = Schema(Capitalize)
|
||||
>>> s('hello world')
|
||||
'Hello world'
|
||||
"""
|
||||
return str(v).capitalize()
|
||||
|
||||
|
||||
def Title(v: str) -> str:
|
||||
"""Title case a string.
|
||||
|
||||
>>> s = Schema(Title)
|
||||
>>> s('hello world')
|
||||
'Hello World'
|
||||
"""
|
||||
return str(v).title()
|
||||
|
||||
|
||||
def Strip(v: str) -> str:
|
||||
"""Strip whitespace from a string.
|
||||
|
||||
>>> s = Schema(Strip)
|
||||
>>> s(' hello world ')
|
||||
'hello world'
|
||||
"""
|
||||
return str(v).strip()
|
||||
|
||||
|
||||
class DefaultTo(object):
|
||||
"""Sets a value to default_value if none provided.
|
||||
|
||||
>>> s = Schema(DefaultTo(42))
|
||||
>>> s(None)
|
||||
42
|
||||
>>> s = Schema(DefaultTo(list))
|
||||
>>> s(None)
|
||||
[]
|
||||
"""
|
||||
|
||||
def __init__(self, default_value, msg: typing.Optional[str] = None) -> None:
|
||||
self.default_value = default_factory(default_value)
|
||||
self.msg = msg
|
||||
|
||||
def __call__(self, v):
|
||||
if v is None:
|
||||
v = self.default_value()
|
||||
return v
|
||||
|
||||
def __repr__(self):
|
||||
return 'DefaultTo(%s)' % (self.default_value(),)
|
||||
|
||||
|
||||
class SetTo(object):
|
||||
"""Set a value, ignoring any previous value.
|
||||
|
||||
>>> s = Schema(validators.Any(int, SetTo(42)))
|
||||
>>> s(2)
|
||||
2
|
||||
>>> s("foo")
|
||||
42
|
||||
"""
|
||||
|
||||
def __init__(self, value) -> None:
|
||||
self.value = default_factory(value)
|
||||
|
||||
def __call__(self, v):
|
||||
return self.value()
|
||||
|
||||
def __repr__(self):
|
||||
return 'SetTo(%s)' % (self.value(),)
|
||||
|
||||
|
||||
class Set(object):
|
||||
"""Convert a list into a set.
|
||||
|
||||
>>> s = Schema(Set())
|
||||
>>> s([]) == set([])
|
||||
True
|
||||
>>> s([1, 2]) == set([1, 2])
|
||||
True
|
||||
>>> with raises(Invalid, regex="^cannot be presented as set: "):
|
||||
... s([set([1, 2]), set([3, 4])])
|
||||
"""
|
||||
|
||||
def __init__(self, msg: typing.Optional[str] = None) -> None:
|
||||
self.msg = msg
|
||||
|
||||
def __call__(self, v):
|
||||
try:
|
||||
set_v = set(v)
|
||||
except Exception as e:
|
||||
raise TypeInvalid(self.msg or 'cannot be presented as set: {0}'.format(e))
|
||||
return set_v
|
||||
|
||||
def __repr__(self):
|
||||
return 'Set()'
|
||||
|
||||
|
||||
class Literal(object):
|
||||
def __init__(self, lit) -> None:
|
||||
self.lit = lit
|
||||
|
||||
def __call__(self, value, msg: typing.Optional[str] = None):
|
||||
if self.lit != value:
|
||||
raise LiteralInvalid(msg or '%s not match for %s' % (value, self.lit))
|
||||
else:
|
||||
return self.lit
|
||||
|
||||
def __str__(self):
|
||||
return str(self.lit)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.lit)
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user