Package: good
GoodSlim yet handsome validation library.Core features:SimpleCustomizableSupports nested model validationError paths (which field contains the error)User-friendly error messagesInternationalization!Robust: 10 000 validations per secondPython 2.7, 3.3+ compatible100% documented and unit-testedInspired by the amazingalecthomas/voluptuousand 100% compatible with it.
The whole internals have been reworked towards readability and robustness. And yeah, the docs are now exhaustive :)The rationale for a remake was to make it modular with a tiny core and everything else built on top of that,
ensure that all error messages are user-friendly out of the box, and tweak the performance.Table of ContentsVoluptuous Drop-In ReplacementSchemaCallablesPrioritiesCreating a SchemaValidatingErrorsInvalidInvalid.enrich()MultipleInvalidMarkersRequiredOptionalRemoveRejectAllowExtraEntireValidation ToolsHelpersObjectMsgTestmessagenametruthPredicatesMaybeAnyAllNeitherInclusiveExclusiveTypesTypeCoerceValuesInLengthDefaultFallbackMapBooleanCheckTruthyFalsyBooleanNumbersRangeClampStringsLowerUpperCapitalizeTitleMatchReplaceUrlEmailDatesDateTimeDateTimeFilesIsFileIsDirPathExistsVoluptuous Drop-In ReplacementDespite Good is modelled after Voluptuous and is highly compatible,
there still are differences that would definitely break your project.If you're not ready for such a change --good.voluptuousis the solution:
compatibility layer for switching fromvoluptuous 0.8.5with 100% compatibility.This is a drop-in replacement that passes all voluptuous unit-tests and hence should work perfectly.
Here's how to use it#from voluptuous import *  # no morefromgood.voluptuousimport*# replacement# .. and use it like beforeIncludes all the features and is absolutely compatible, except for the error message texts,
which became much more user-friendly :)Migration steps:Replacevoluptuousimports withgood.voluptuousRun your application tests and see how it behavesModule by module, replacegood.voluptuouswith justgood, keeping the differences in mind.Also note the small differences that are still present:Settings forrequiredandextraare not inherited by embedded mappings.If your top-level schema definesrequired=False, embedded mappings will still have the defaultrequired=True!
And same withextra.Different error message texts, which are easier to understand :)RaisesInvalidrather thanMultipleInvalidfor rejected extra mapping keys (seeExtra)Good luck! :)SchemaValidation schema.A schema is a Python structure where nodes are pattern-matched against the corresponding values.
It leverages the full flexibility of Python, allowing you to match values, types, data structures and much more.When a schema is created, it's compiled into a callable function which does the validation, hence it does not need
to analyze the schema every time.Once the Schema is defined, validation can be triggered by calling it:fromgoodimportSchemaschema=Schema({'a':str})# Testschema({'a':'i am a valid string'})The following rules exist:Literal: plain value is validated with direct comparison (equality check):Schema(1)(1)#-> 1Schema(1)(2)#-> Invalid: Invalid value: expected 1, got 2Type: type schema produces a stricttype(v) == schemacheck on the input value:Schema(int)(1)#-> 1Schema(int)(True)#-> Invalid: Wrong type: expected Integer number, got BooleanSchema(int)('1')#-> Invalid: Wrong type: expected Integer number, got Binary StringFor Python2, there is an exception forbasestring: it won't make strict type checks, but ratherisinstance().For a relaxedisinstance()check, seeTypevalidator.Enum:Python 3.4 Enums,
or the backportedenum34.Tests whether the input value is a validEnumvalue:fromenumimportEnumclassColors(Enum):RED=0xFF0000GREEN=0x00FF00BLUE=0x0000FFschema=Schema(Colors)schema(0xFF0000)#-> <Colors.RED: 0xFF0000>schema(Colors.RED)#-> <Colors.RED: 0xFF0000>schema(123)#-> Invalid: Invalid Colors value, expected Colors, got 123Output is always an instance of the providedEnumtype value.Callable: is applied to the value and the result is used as the final value.Callables should raiseInvaliderrors in case of a failure, however some generic error types are
converted automatically: seeCallables.In addition, validators are allowed to transform a value to the required form.
For instance,Coerce(int)returns a callable which will convert input values intointor fail.defCoerceInt(v):# naive Coerce(int) implementationreturnint(v)Schema(CoerceInt)(1)#-> 1Schema(CoerceInt)('1')#-> 1Schema(CoerceInt)('a')#-> Invalid: invalid literal for int(): expected CoerceInt(), got aSchema: a schema may contain sub-schemas:sub_schema=Schema(int)schema=Schema([None,sub_schema])schema([None,1,2])#-> [None, 1, 2]schema([None,'1'])#-> Invalid: invalid valueSinceSchemais callable, validation transparently by just calling it :)Moreover, instances of the following types are converted to callables on the compilation phase:Iterables(list,tuple,set, custom iterables):Iterables are treated as a set of valid values,
where each value in the input is compared against each value in the schema.In order for the input to be valid, it needs to have the same iterable type, and all of its
values should have at least one matching value in the schema.schema=Schema([1,2,3])# List of valid valuesschema([1,2,2])#-> [1, 2, 2]schema([1,2,4])#-> Invalid: Invalid value @ [2]: expected List[1|2|3], got 4schema((1,2,2))#-> Invalid: Wrong value type: expected List, got TupleEach value within the iterable is a schema as well, and validation requires that
each member of the input value matchesanyof the schemas.
Thus, an iterable is a way to defineORvalidation rule for every member of the iterable:Schema([# All values should be# .. int ..int,# .. or a string, casted to int ..lambdav:int(v)])([1,2,'3'])#-> [ 1, 2, 3 ]This example works like this:Validate that the input value has the matching type:listin this caseFor every member of the list, test that there is a matching value in the schema.E.g. for value1--intmatches (immediateinstanceof()check).
However, for value'3'--intfails, but the callable manages to do it with no errors,
and transforms the value as well.Since lists are ordered, the first schema that didn't fail is used.Mappings(dict, custom mappings):Each key-value pair in the input mapping is validated against the corresponding schema pair:Schema({'name':str,'age':lambdav:int(v)})({'name':'Alex','age':'18',})#-> {'name': 'Alex', 'age': 18}When validating,bothkeys and values are schemas, which allows to use nested schemas and interesting validation rules.
For instance, let's useInvalidator to match certain keys:fromgoodimportSchema,InSchema({# These two keys should have integer valuesIn({'age','height'}):int,# All other string keys (other than 'age', 'height') should have string valuesAll(str,Neither(In({'age','height'}))):str,})({'age':18,'height':173,'name':'Alex',})This works like this:Test that the input has a matching type (dict)For each key in the input mapping, matching keys are selected from the schemaValidate input values with the corresponding value in the schema.In addition, certain keys can be marked asRequiredandOptional.
The default behavior is to have all keys required, but this can be changed by providingdefault_keys=Optionalargument to the Schema.Finally, a mapping does not allow any extra keys (keys not defined in the schema). To change this, provideextra_keys=Allowto theSchemaconstructor.Please note thatdefault_keysandextra_keyssettings do not propagate to sub-schemas and are only applied
to the top-level mapping. If required, wrap sub-schemas with anotherSchema()and feed the settings, or
useMarkersexplicitly.These are just the basic rules, and for sureSchemacan do much more than that!
Additional logic is implemented throughMarkersandValidators,
which are described in the following chapters.CallablesFinally, here are the things to consider when using custom callables for validation:Throwing errors.If the callable throwsInvalidexception, it's used as is with all the rich info it provides.
Schema is smart enough to fill into most of the arguments (seeInvalid.enrich),
so it's enough to use a custom message, and probably, set a human-friendlyexpectedfield.In addition, specific error types are wrapped intoInvalidautomatically: these areAssertionError,TypeError,ValueError.
Schema tries to do its best, but such messages will probably be cryptic for the user.
Hence, always raise meaningful errors when creating custom validators.
Still, this opens the possibility to use Python typecasting with validators likelambda v: int(v),
since most of them are throwingTypeErrororValueError.Naming.If the provided callable does not specifyInvalid.expectedexpected value,
the__name__of the callable is be used instead.
E.g.def intify(v):passbecomes'intify()'in reported errors.If a custom name is desired on the callable -- set thenameattribute on the callable object.
This works best with classes, however a function can acceptnameattribute as well.For convenience,@messageand@namedecorators can be used on callables
to specify the name and override the error message used when the validator fails.Signals.A callable may decide that the value is soooo invalid that it should be dropped from the sanitized output.
In this case, the callable should raisegood.schema.signals.RemoveValue.This is used by theRemove()marker, but can be leveraged by other callables as well.PrioritiesEvery schema type has a priority (source),
which define the sequence for matching keys in a mapping schema:Literals have highest priorityTypes has lower priorities than literals, hence schemas can define specific rules for individual keys,
and then declare general rules by type-matching:Schema({'name':str,# Specific rule with a literalstr:int,# General rule with a type})Callables, iterables, mappings -- have lower priorities.In addition,Markershave individual priorities,
which can be higher that literals (Remove()marker) or lower than callables (Extramarker).Creating a SchemaSchema(schema,default_keys=None,extra_keys=None)Creates a compiledSchemaobject from the given schema definition.Under the hood, it usesSchemaCompiler: see thesourceif interested.Arguments:schema: Schema definitiondefault_keys: Default mapping keys behavior:
aMarkerclass used as a default on mapping keys which are not Marker()ed with anything.Defaults tomarkers.Required.extra_keys: Default extra keys behavior: sub-schema, or aMarkerclass.Defaults tomarkers.RejectThrows:SchemaError: Schema compilation errorValidatingSchema.__call__(value)Having aSchema, user input can be validated by calling the Schema on the input value.When called, the Schema will return sanitized value, or raise exceptions.Arguments:value: Input value to validateReturns:NoneSanitized valueThrows:good.MultipleInvalid: Validation error on multiple values. SeeMultipleInvalid.good.Invalid: Validation error on a single value. SeeInvalid.ErrorsSource:good/schema/errors.pyWhenvalidating user input,Schemacollects all errors and throws these
after the whole input value is validated. This makes sure that you can reportallerrors at once.With simple schemas, likeSchema(int), only a single error is available: e.g. wrong value type.
In this case,Invaliderror is raised.However, with complex schemas with embedded structures and such, multiple errors can occur:
then [MultipleInvalid] is reported.All errors are available right at the top-level:fromgoodimportInvalid,MultipleInvalidInvalidInvalid(message,expected=None,provided=None,path=None,validator=None,**info)Validation error for a single value.This exception is guaranteed to contain text values which are meaningful for the user.Arguments:message: Validation error message.expected: Expected value: info about the value the validator was expecting.If validator does not specify it -- the name of the validator is used.provided: Provided value: info about the value that was actually supplied by the userIf validator does not specify it -- the input value is typecasted to string and stored here.path: Path to the error value.E.g. if an invalid value was encountered at ['a'].b[1], then path=['a', 'b', 1].validator: The validator that has failed: a schema item**info: Custom values that might be provided by the validator. No built-in validator uses this.Invalid.enrich()Invalid.enrich(expected=None,provided=None,path=None,validator=None)Enrich this error with additional information.This works with both Invalid and MultipleInvalid (thanks toInvalidbeing iterable):
in the latter case, the defaults are applied to all collected errors.The specified arguments are only set onInvaliderrors which do not have any value on the property.One exclusion ispath: if provided, it is prepended toInvalid.path.
This feature is especially useful when validating the whole input with multiple different schemas:fromgoodimportSchema,Invalidschema=Schema(int)input={'user':{'age':10,}}try:schema(input['user']['age'])exceptInvalidase:e.enrich(path=['user','age'])# Make the path reflect the realityraise# re-raise the error with updated fieldsThis is used when validating a value within a container.Arguments:expected: Invalid.expected defaultprovided: Invalid.provided defaultpath: Prefix to prepend to Invalid.pathvalidator: Invalid.validator defaultReturns:Invalid|MultipleInvalidMultipleInvalidMultipleInvalid(errors)Validation errors for multiple values.This error is raised when theSchemahas reported multiple errors, e.g. for several dictionary keys.MultipleInvalidhas the same attributes asInvalid,
but the values are taken from the first error in the list.In addition, it has theerrorsattribute, which is a list ofInvaliderrors collected by the schema.
The list is guaranteed to be plain: e.g. there will be no underlying hierarchy ofMultipleInvalid.Note that bothInvalidandMultipleInvalidare iterable, which allows to process them in singularity:try:schema(input_value)exceptInvalidasee:reported_problems={}foreinee:# Iterate over `Invalid`path_str=u'.'.join(e.path)# 'a.b.c.d', JavaScript-friendly :)reported_problems[path_str]=e.message#.. send reported_problems to the userIn this example, we create a dictionary of paths (as strings) mapped to error strings for the user.Arguments:errors: The reported errors.If it containsMultipleInvaliderrors -- the list is recursively flattened
so all of them are guaranteed to be instances ofInvalid.MarkersAMarkeris a proxy class which wraps some schema.Immediately, the example is:fromgoodimportSchema,RequiredSchema({'name':str,# required keyOptional('age'):int,# optional key},default_keys=Required)This way, keys marked withRequired()will report errors if no value if provided.Typically, a marker "decorates" a mapping key, but some of them can be "standalone":fromgoodimportSchema,ExtraSchema({'name':str,Extra:int# allow any keys, provided their values are integer})Each marker can have it's own unique behavior since nothing is hardcoded into the coreSchema.
Keep on reading to learn how markers perform.RequiredRequired(key)Required(key)is used to decorate mapping keys and hence specify that these keys must always be present in
the input mapping.When compiled,Schemausesdefault_keysas the default marker:fromgoodimportSchema,Requiredschema=Schema({'name':str,'age':int},default_keys=Required)# wrap with Required() by defaultschema({'name':'Mark'})#-> Invalid: Required key not provided @ ['age']: expected age, got -none-Remember that mapping keys are schemas as well, andRequirewill expect to always have a match:schema=Schema({Required(str):int,})schema({})# no `str` keys provided#-> Invalid: Required key not provided: expected String, got -none-In addition, theRequiredmarker has special behavior withDefaultthat allows to set the key
to a default value if the key was not provided. More details in the docs forDefault.Arguments:OptionalOptional(key)Optional(key)is controversial toRequired(key): specified that the mapping key is not required.This only has meaning when aSchemahasdefault_keys=Required:
then, it decorates all keys withRequired(), unless a key is already decorated with some Marker.Optional()steps in: those keys are already decorated and hence are not wrapped withRequired().So, it's only used to preventSchemafrom puttingRequired()on a key.
In all other senses, it has absolutely no special behavior.As a result, optional key can be missing, but if it was provided -- its value must match the value schema.Example: use asdefault_keys:schema=Schema({'name':str,'age':int},default_keys=Optional)# Make all keys optional by defaultschema({})#-> {} -- okayschema({'name':None})#->  Invalid: Wrong type @ ['name']: expected String, got NoneExample: use to mark specific keys are not required:schema=Schema({'name':str,Optional(str):int# key is optional})schema({'name':'Mark'})# validschema({'name':'Mark','age':10})# validschema({'name':'Mark','age':'X'})#-> Invalid: Wrong type @ ['age']: expected Integer number, got Binary StringArguments:RemoveRemove(key)Remove(key)marker is used to declare that the key, if encountered,
should be removed, without validating the value.Removehas highest priority, so it operates before everything else in the schema.Example:schema=Schema({Remove('name'):str,# `str` does not mean anything since the key is removed anyway'age':int})schema({'name':111,'age':18})#-> {'age': 18}However, it's more natural to useRemove()on values.
Remember that in this case'name'will becomeRequired(),
if not decorated withOptional():schema=Schema({Optional('name'):Remove})schema({'name':111,'age':18})#-> {'age': 18}Bonus:Remove()can be used in iterables as well:schema=Schema([str,Remove(int)])schema(['a','b',1,2])#-> ['a', 'b']Arguments:RejectReject(key)Reject(key)marker is used to reportInvaliderrors every time is matches something in the input.It has lower priority than most of other schemas, so rejection will only happen
if no other schemas has matched this value.Example:schema=Schema({Reject('name'):None,# Reject by keyOptional('age'):Msg(Reject,u"Field is not supported anymore"),# alternative form})schema({'name':111})#-> Invalid: Field is not supported anymore @ ['name']: expected -none-, got nameArguments:AllowAllow(key)Allow(key)is a no-op marker that never complains on anything.Designed to be used withExtra.Arguments:ExtraExtra(key)Extrais a catch-all marker to define the behavior for mapping keys not defined in the schema.It has the lowest priority, and delegates its function to its value, which can be a schema, or another marker.Given without argument, it's compiled with an identity functionlambda x:xwhich is a catch-all:
it matches any value. Together with lowest priority,Extrawill only catch values which did not match anything else.Every mapping has anExtraimplicitly, andextra_keysargument controls the default behavior.Example withExtra: <schema>:schema=Schema({'name':str,Extra:int# this will allow extra keys provided they're int})schema({'name':'Alex','age':18'})  #-> okschema({'name':'Alex','age':'X'})#-> Invalid: Wrong type @ ['age']: expected Integer number, got Binary StringExample withExtra: Reject: reject all extra values:schema=Schema({'name':str,Extra:Reject})schema({'name':'Alex','age':'X'})#-> Invalid: Extra keys not allowed @ ['age']: expected -none-, got ageExample withExtra: Remove: silently discard all extra values:schema=Schema({'name':str},extra_keys=Remove)schema({'name':'Alex','age':'X'})#-> {'name': 'Alex'}Example withExtra: Allow: allow any extra values:schema=Schema({'name':str},extra_keys=Allow)schema({'name':'Alex','age':'X'})#-> {'name': 'Alex', 'age': 'X'}Arguments:EntireEntire(key)Entireis a convenience marker that validates the entire mapping using validators provided as a value.It has absolutely lowest priority, lower thanExtra, hence it never matches any keys, but is still executed to
validate the mapping itself.This opens the possibilities to define rules on multiple fields.
This feature is leveraged by theInclusiveandExclusivegroup validators.For example, let's require the mapping to have no more than 3 keys:fromgoodimportSchema,Entiredefmaxkeys(n):# Return a validator functiondefvalidator(d):# `d` is the dictionary.# Validate itassertlen(d)<=3,'Dict size should be <= 3'# Return the value since all callable schemas should do thatreturndreturnvalidatorschema=Schema({str:int,Entire:maxkeys(3)})In this example,Entireis executed for every input dictionary, and magically calls the schema it's mapped to.
Themaxkeys(n)schema is a validator that complains on the dictionary size if it's too huge.Schemacatches theAssertionErrorthrown by it and converts it toInvalid.Note that the schema this marker is mapped to can't replace the mapping object, but it can mutate the given mapping.Arguments:Validation ToolsAll validators listed here inherit fromValidatorBasewhich defines the standard interface.
Currently it makes no difference whether it's just a callable, a class, or a subclass ofValidatorBase,
but in the future it may gain special features.HelpersCollection of miscellaneous helpers to alter the validation process.ObjectObject(schema,cls=None)Specify that the provided mapping should validate an object.This uses the same mapping validation rules, but works with attributes instead:fromgoodimportSchema,Objectintify=lambdav:int(v)# Naive Coerce(int) implementation# Define a class to play withclassPerson:category=u'Something'# Not validateddef__init__(self,name,age):self.name=nameself.age=age# Schemaschema=Schema(Object({'name':str,'age':intify,}))# Validateschema(Person(name=u'Alex',age='18'))#-> Girl(name=u'Alex', age=18)Internally, it validates the object's__dict__: hence, class attributes are excluded from validation.
Validation is performed with the help of a wrapper class which proxies object attributes as mapping keys,
and then Schema validates it as a mapping.This inherits the default required/extra keys behavior of the Schema.
To override, useOptional()andExtramarkers.Arguments:schema: Object schema, given as a mappingcls: Require instances of a specific class. IfNone, allows all classes.MsgMsg(schema,message)Override the error message reported by the wrapped schema in case of validation errors.On validation, if the schema throwsInvalid-- the message is overridden withmsg.Some other error types are converted toInvalid: see notes onSchema Callables.fromgoodimportSchema,Msgintify=lambdav:int(v)# Naive Coerce(int) implementationintify.name=u'Number'schema=Schema(Msg(intify,u'Need a number'))schema(1)#-> 1schema('a')#-> Invalid: Need a number: expected Number, got aArguments:schema: The wrapped schema to modify the error formessage: Error message to use instead of the one that's reported by the underlying schemaTestTest(fun)Test the value with the provided function, expecting that it won't throw errors.If no errors were thrown -- the value is valid andthe original input value is used.
If any error was thrown -- the value is considered invalid.This is especially useful to discard tranformations made by the wrapped validator:fromgoodimportSchema,Coerceschema=Schema(Coerce(int))schema(123)#-> 123schema('123')#-> '123' -- still stringschema('abc')#-> Invalid: Invalid value, expected *Integer number, got abcArguments:fun: Callable to test the value with, or a validator function.Note that this won't work with mutable input values since they're modified in-place!messagemessage(message,name=None)Convenience decorator that appliesMsg()to a callable.fromgoodimportSchema,message@message(u'Need a number')defintify(v):returnint(v)Arguments:message: Error message to use insteadname: Override schema name as well. Seename.Returns:callabledecoratornamename(name,validator=None)Set a name on a validator callable.Useful for user-friendly reporting when using lambdas to populate theInvalid.expectedfield:fromgoodimportSchema,nameSchema(lambdax:int(x))('a')#-> Invalid: invalid literal for int(): expected <lambda>(), gotSchema(name('int()',lambdax:int(x))('a')#-> Invalid: invalid literal for int(): expected int(), got aNote that it is only useful with lambdas, since function name is used if available:
see notes onSchema Callables.Arguments:name: Name to assign on the validator callablevalidator: Validator callable. If not provided -- a decorator is returned instead:fromgoodimportname@name(u'int()')defint(v):returnint(v)Returns:callableThe same validator callabletruthtruth(message,expected=None)Convenience decorator that appliesCheckto a callable.fromgoodimporttruth@truth(u'Must be an existing directory')defisDir(v):returnos.path.isdir(v)Arguments:message: Validation error messageexpected: Expected value string representation, orNoneto get it from the wrapped callableReturns:callabledecoratorPredicatesMaybeMaybe(schema,none=None)Validate the the value either matches the given schema or is None.This supportsnullablevalues and gives them a good representation.fromgoodimportSchema,Maybe,Emailschema=Schema(Maybe(Email))schema(None)#-> Noneschema('user@example.com')#-> 'user@example.com'scheam('blahblah')#-> Invalid: Wrong E-Mail: expected E-Mail?, got blahblahNote that it also have theDefault-like behaviorthat initializes the missingRequired()keys:schema=Schema({'email':Maybe(Email)})schema({})#-> {'email': None}Arguments:schema: Schema for a provided valuenone: Empty value literalAnyAny(*schemas)Try the provided schemas in order and use the first one that succeeds.This is theORcondition predicate: any of the schemas should match.Invaliderror is reported if neither of the schemas has matched.fromgoodimportSchema,Anyschema=Schema(Any(# allowed string constants'true','false',# otherwise coerce as a boollambdav:'true'ifvelse'false'))schema('true')#-> 'true'schema(0)#-> 'false'Arguments:*schemas: List of schemas to try.AllAll(*schemas)Value must pass all validators wrapped withAll()predicate.This is theANDcondition predicate: all of the schemas should match in order,
which is in fact a composition of validators:All(f,g)(value) = g(f(value)).fromgoodimportSchema,All,Rangeschema=Schema(All(# Must be an integer ..int,# .. and in the allowed rangeRange(0,10)))schema(1)#-> 1schema(99)#-> Invalid: Not in range: expected 0..10, got 99Arguments:*schemas: List of schemas to apply.NeitherNeither(*schemas)Value must not match any of the schemas.This is theNOTcondition predicate: a value is considered valid if each schema has raised an error.fromgoodimportSchema,All,Neitherschema=Schema(All(# Integerint,# But not zeroNeither(0)))schema(1)#-> 1schema(0)#-> Invalid: Value not allowed: expected Not(0), got 0Arguments:*schemas: List of schemas to check against.InclusiveInclusive(*keys)Inclusivevalidates the defined inclusive group of mapping keys:
if any of them was provided -- then all of them become required.This exists to support "sub-structures" within the mapping which only make sense if specified together.
Since this validator works on the entire mapping, the best way is to use it together with theEntiremarker:fromgoodimportSchema,Entire,Inclusiveschema=Schema({# Fields for all files'name':str,# Fields for images onlyOptional('width'):int,Optional('height'):int,# Now put a validator on the entire mappingEntire:Inclusive('width','height')})schema({'name':'monica.jpg'})#-> okschema({'name':'monica.jpg','width':800,'height':600})#-> okschema({'name':'monica.jpg','width':800})#-> Invalid: Required key not provided: expected height, got -none-Note thatInclusiveonly supports literals.Arguments:*keys: List of mutually inclusive keys (literals).ExclusiveExclusive(*keys)Exclusivevalidates the defined exclusive group of mapping keys:
if any of them was provided -- then none of the remaining keys can be used.This supports "sub-structures" with choice: if the user chooses a field from one of them --
then he cannot use others.
It works on the entire mapping and hence best to use with theEntiremarker.By default,Exclusiverequires the user to choose one of the options,
but this can be overridden withOptionalmarker class given as an argument:fromgoodimportExclusive,Required,Optional# Requires either of themExclusive('login','password')Exclusive(Required,'login','password')# the default# Requires either of them, or noneExclusive(Optional,'login','password')Let's demonstrate with the API that supports multiple types of authentication,
but requires the user to choose just one:fromgoodimportSchema,Entire,Exclusiveschema=Schema({# Authentication types: login+password | email+passwordOptional('login'):str,Optional('email'):str,'password':str,# Now put a validator on the entire mapping# that forces the user to chooseEntire:Msg(# also override the messageExclusive('login','email'),u'Choose one')})schema({'login':'kolypto','password':'qwerty'})#-> okschema({'email':'kolypto','password':'qwerty'})#-> okschema({'login':'a','email':'b','password':'c'})#-> MultipleInvalid:#->     Invalid: Choose one @ [login]: expected login|email, got login#->     Invalid: Choose one @ [email]: expected login|email, got emailNote thatExclusiveonly supports literals.Arguments:*keys: List of mutually exclusive keys (literals).Can containRequiredorOptionalmarker classes,
which defines the behavior when no keys are provided. Default isRequired.TypesTypeType(*types)Check if the value has the specific type withisinstance()check.In contrast toSchema typeswhich performs a strict check, this check is relaxed and accepts subtypes
as well.fromgoodimportSchema,Typeschema=Schema(Type(int))schema(1)#-> 1schema(True)#-> TrueArguments:*types: The type to check instances against.If multiple types are provided, then any of them is acceptable.CoerceCoerce(constructor)Coerce a value to a type with the provided callable.Coerceapplies theconstructorto the input value and returns a value cast to the provided type.Ifconstructorfails withTypeErrororValueError, the value is considered invalid andCoercecomplains
on that with a custom message.However, ifconstructorraisesInvalid-- the error object is used as it.fromgoodimportSchema,Coerceschema=Schema(Coerce(int))schema(u'1')#-> 1schema(u'a')#-> Invalid: Invalid value: expected *Integer number, got aArguments:constructor: Callable that typecasts the input valueValuesInIn(container)Validate that a value is in a collection.This is a plain simplevalue in containercheck, wherecontaineris a collection of literals.In contrast toAny, it does not compile its arguments into schemas,
and hence achieves better performance.fromgoodimportSchema,Inschema=Schema(In({1,2,3}))schema(1)#-> 1schema(99)#-> Invalid: Unsupported value: expected In(1,2,3), got 99The same example will work withAny, but slower :-)Arguments:container: Collection of allowed values.In addition to naive tuple/list/set/dict, this can be any object that supportsinoperation.LengthLength(min=None,max=None)Validate that the provided collection has length in a certain range.fromgoodimportSchema,Lengthschema=Schema(All(# Ensure it's a list (and not any other iterable type)list,# Validate lengthLength(max=3),))Since mappings also have length, they can be validated as well:schema=Schema({# Strings mapped to integersstr:int,# Size = 1..3# Empty dicts are not allowed since `str` is implicitly `Required(str)`Entire:Length(max=3)})schema([1])#-> okschema([1,2,3,4])#-> Invalid: Too long (3 is the most): expected Length(..3), got 4Arguments:min: Minimal allowed length, orNoneto impose no limits.max: Maximal allowed length, orNoneto impose no limits.DefaultDefault(default)Initialize a value to a default if it's not provided."Not provided" meansNone, so basically it replacesNones with the default:fromgoodimportSchema,Any,Defaultschema=Schema(Any(# Accept intsint,# Replace `None` with 0Default(0)))schema(1)#-> 1schema(None)#-> 0It raisesInvalidon all values except forNoneanddefault:schema=Schema(Default(42))schema(42)#-> 42schema(None)#-> 42schema(1)#-> Invalid: Invalid valueIn addition,Defaulthas special behavior withRequiredmarker which is built into it:
if a required key was not provided -- it's created with the default value:fromgoodimportSchema,Defaultschema=Schema({# remember that keys are implicitly required'name':str,'age':Any(int,Default(0))})schema({'name':'Alex'})#-> {'name': 'Alex', 'age': 0}Arguments:default: The default value to useFallbackFallback(default)Always returns the default value.Works likeDefault, but does not fail on any values.Typical usage is to terminateAnychain in case nothing worked:fromgoodimportSchema,Any,Fallbackschema=Schema(Any(int,# All non-integer numbers are replaced with `None`Fallback(None)))LikeDefault, it also works with mappings.Internally,DefaultandFallbackwork by feeding the schema with a specialUndefinedvalue:
if the schema manages to return some value without errors -- then it has the named "default behavior",
and this validator just leverages the feature.A "fallback value" may be provided manually, and will work absolutely the same
(since value schema manages to succeed even thoughUndefinedwas given):schema=Schema({'name':str,'age':Any(int,lambdav:42)})Arguments:default: The value that's always returnedMapMap(enum,mode=1)Convert Enumerations that map names to values.Supports three kinds of enumerations:Mapping.Provided a mapping from names to values,
converts the input to values by mapping key:fromgoodimportSchema,Mapschema=Schema(Map({'RED':0xFF0000,'GREEN':0x00FF00,'BLUE':0x0000FF}))schema('RED')#-> 0xFF0000schema('BLACK')#-> Invalid: Unsupported value: expected Constant, provided BLACKClass.Provided a class with attributes (names) initialized with values,
converts the input to values matching by attribute name:classColors:RED=0xFF0000GREEN=0x00FF00BLUE=0x0000FFschema=Schema(Map(Colors))schema('RED')#-> 0xFF0000schema('BLACK')#-> Invalid: Unsupported value: expected Colors, provided BLACKNote that all attributes of the class are used, except for protected (_name) and callables.Enum.SupportsPython 3.4 Enumsand the backportedenum34.Provided an enumeration, converts the input to values by name.
In addition, enumeration value can pass through safely:fromenumimportEnumclassColors(Enum):RED=0xFF0000GREEN=0x00FF00BLUE=0x0000FFschema=Schema(Map(Colors))schema('RED')#-> <Colors.RED: 0xFF0000>schema('BLACK')#-> Invalid: Unsupported value: expected Colors, provided BLACKNote that inmode=Map.VALit works precisely likeSchema(Enum).In addition to the "straignt" mode (lookup by key), it supports reverse matching:Whenmode=Map.KEY, does only forward matching (by key) -- the defaultWhenmode=Map.VAL, does only reverse matching (by value)Whenmode=Map.BOTH, does bidirectional matching (by key first, then by value)Another neat feature is thatMapsupportsincontainment checks,
which works great together withIn:In(Map(enum-value))will test if a value is convertible, but won't
actually do the convertion.fromgoodimportSchema,Map,Inschema=Schema(In(Map(Colors)))schema('RED')#-> 'RED'schema('BLACK')#-> Invalid: Unsupported value, expected Colors, got BLACKArguments:enum: Enumeration: dict, object, of Enummode: Matching mode: one of Map.KEY, Map.VAL, Map.BOTHBooleanCheckCheck(bvalidator,message,expected)Use the provided boolean function as a validator and raise errors when it'sFalse.importos.pathfromgoodimportSchema,Checkschema=Schema(Check(os.path.isdir,u'Must be an existing directory'))schema('/')#-> '/'schema('/404')#-> Invalid: Must be an existing directory: expected isDir(), got /404Arguments:bvalidator: Boolean validator functionmessage: Error message to report whenFalseexpected: Expected value string representation, orNoneto get it from the wrapped callableTruthyTruthy()Assert that the value is truthy, in the Python sense.This fails on all "falsy" values:False,0, empty collections, etc.fromgoodimportSchema,Truthyschema=Schema(Truthy())schema(1)#-> 1schema([1,2,3])#-> [1,2,3]schema(None)#-> Invalid: Empty value: expected truthy(), got NoneFalsyFalsy()Assert that the value is falsy, in the Python sense.Supplementary toTruthy.BooleanBoolean()Convert human-readable boolean values to abool.The following values are supported:None:Falsebool: directint:0=False, everything else isTruestr: Textual boolean values, compatible withYAML 1.1 boolean literals, namely:y|Y|yes|Yes|YES|n|N|no|No|NO|
  true|True|TRUE|false|False|FALSE|
  on|On|ON|off|Off|OFFInvalidis thrown if an unknown string literal is provided.Example:fromgoodimportSchema,Booleanschema=Schema(Boolean())schema(None)#-> Falseschema(0)#-> Falseschema(1)#-> Trueschema(True)#-> Trueschema(u'yes')#-> TrueNumbersRangeRange(min=None,max=None)Validate that the value is within the defined range, inclusive.
RaiseInvaliderror if not.fromgoodimportSchema,Rangeschema=Schema(Range(1,10))schema(1)#-> 1schema(10)#-> 10schema(15)#-> Invalid: Value must be at most 10: expected Range(1..10), got 15If the value cannot be compared to a number -- raisesInvalid.
Note that in Python2 almost everything can be compared to a number, including strings, dicts and lists!Arguments:min: Minimal allowed value, orNoneto impose no limits.max: Maximal allowed value, orNoneto impose no limits.ClampClamp(min=None,max=None)Clamp a value to the defined range, inclusive.fromgoodimportSchema,Clampschema=Schema(Clamp(1,10))schema(-1)#-> 1schema(1)#-> 1schema(10)#-> 10schema(15)#-> 10If the value cannot be compared to a number -- raisesInvalid.
Note that in Python2 almost everything can be compared to a number, including strings, dicts and lists!Arguments:min: Minimal allowed value, orNoneto impose no limits.max: Maximal allowed value, orNoneto impose no limits.StringsLowerLower()Casts the provided string to lowercase, fails is the input value is not a string.Supports both binary and unicode strings.fromgoodimportSchema,Lowerschema=Schema(Lower())schema(u'ABC')#-> u'abc'schema(123)#-> Invalid: Not a string: expected String, provided Integer numberUpperUpper()Casts the input string to UPPERCASE.CapitalizeCapitalize()Capitalizes the input string.TitleTitle()Casts The Input String To Title CaseMatchMatch(pattern,message=None,expected=None)Validate the input string against a regular expression.fromgoodimportSchema,Matchschema=Schema(All(unicode,Match(r'^0x[A-F0-9]+$','hex number')))schema('0xDEADBEEF')#-> '0xDEADBEEF'schema('0x')#-> Invalid: Wrong format: expected hex number, got 0xDEADBEEFArguments:pattern: RegExp pattern to match with: a string, or a compiled patternmessage: Error message overrideexpected: Textual representation of what's expected from the userReplaceReplace(pattern,repl,message=None,expected=None)RegExp substitution.fromgoodimportSchema,Replaceschema=Schema(Replace(# Grab domain namer'^https?://([^/]+)/.*'# Replacer'',# Tell the user that we're expecting a URLu'URL'))schema('http://example.com/a/b/c')#-> 'example.com'schema('user@example.com')#-> Invalid: Wrong format: expected URL, got user@example.comArguments:pattern: RegExp pattern to match with: a string, or a compiled patternrepl: Replacement pattern.Backreferences are supported, just like in theremodule.message: Error message overrideexpected: Textual representation of what's expected from the userUrlUrl(protocols=('http','https'))Validate a URL, make sure it's in the absolute format, including the protocol.fromgoodimportSchema,Urlschema=Schema(Url('https'))schema('example.com')#-> 'https://example.com'schema('http://example.com')#-> 'http://example.com'Arguments:protocols: List of allowed protocols.If no protocol is provided by the user -- the first protocol is used by default.EmailEmail()Validate that a value is an e-mail address.This simply tests for the presence of the '@' sign, surrounded by some characters.fromgoodimportEmailschema=Schema(Email())schema('user@example.com')#-> 'user@example.com'schema('user@localhost')#-> 'user@localhost'schema('user')#-> Invalid: Invalid e-mail: expected E-Mail, got userDatesDateTimeDateTime(formats,localize=None,astz=None)Validate that the input is a Pythondatetime.Supports the following input values:datetime: passthroughstring: parses the string with any of the specified formats
(seestrptime())fromdatetimeimportdatetimefromgoodimportSchema,DateTimeschema=Schema(DateTime('%Y-%m-%d%H:%M:%S'))schema('2014-09-06 21:22:23')#-> datetime.datetime(2014, 9, 6, 21, 22, 23)schema(datetime.now())#-> datetime.datetime(2014, 9, 6, 21, 22, 23)schema('2014')#-> Invalid: Invalid datetime format, expected DateTime, got 2014.Notes on timezones:If the format does not support timezones, it always returnsnaivedatetimeobjects (withouttzinfo).If timezones are supported by the format (with%z/%Z),
it returns anawaredatetimeobjects (withtzinfo).Since Python2 does not always support%z--DateTimedoes this manually.
Due to the limited nature of this workaround, the support for%zonly works if it's at the end of the string!As a result, '00:00:00' is parsed into anaivedatetime, and '00:00:00 +0200' results in anawaredatetime.If your application wants different rules, uselocalizeandastz:localizeargument is the default timezone to set onnaivedatetimes,
or a callable which is applied to the input and should return adjusteddatetime.astzargument is the timezone to adjust theawaredatetime to, or a callable.Then the generic recipe is:Setlocalizeto the timezone (or a callable) that you expect the user to input the datetime inSetastzto the timezone you wish to have in the result.This works best with the excellentpytzlibrary:importpytzfromgoodimportSchema,DateTime# Formats: with and without timezoneformats=['%Y-%m-%d%H:%M:%S','%Y-%m-%d%H:%M:%S%z']# The used timezonesUTC=pytz.timezone('UTC')Oslo=pytz.timezone('Europe/Oslo')### Example: Use Europe/Oslo by defaultschema=Schema(DateTime(formats,localize=Oslo))schema('2014-01-01 00:00:00')#-> datetime.datetime(2014, 1, 1, 0, 0, tzinfo='Europe/Oslo')schema('2014-01-01 00:00:00-0100')#-> datetime.datetime(2014, 1, 1, 0, 0, tzinfo=-0100)### Example: Use Europe/Oslo by default and convert to an aware UTCschema=Schema(DateTime(formats,localize=Oslo,astz=UTC))schema('2014-01-01 00:00:00')#-> datetime.datetime(2013, 12, 31, 23, 17, tzinfo=<UTC>)schema('2014-01-01 00:00:00-0100')#-> datetime.datetime(2014, 1, 1, 1, 0, tzinfo=<UTC>)### Example: Use Europe/Oslo by default, convert to a naive UTC# This is the recommended wayschema=Schema(DateTime(formats,localize=Oslo,astz=lambdav:v.astimezone(UTC).replace(tzinfo=None)))schema('2014-01-01 00:00:00')#-> datetime.datetime(2013, 12, 31, 23, 17)schema('2014-01-01 00:00:00-0100')#-> datetime.datetime(2014, 1, 1, 1, 0)Note: to save some pain, make sure toalwayswork with naivedatetimesadjusted to UTC!
Armin Ronacherexplains it here.Summarizing all the above, the validation procedure is a 3-step process:Parse (only with strings)If isnaive-- applylocalizeand make itaware(iflocalizeis specified)If isaware-- applyastzto convert it (ifastzis specified)Arguments:formats: Supported format string, or an iterable of formats to try them all.localize: Adjustnaivedatetimesto a timezone, making itaware.Atzinfotimezone object,
or a callable which is applied to anaivedatetime and should return an adjusted value.Only called fornaivedatetimes.astz: Adjustawaredatetimesto another timezone.Atzinfotimezone object,
or a callable which is applied to anawaredatetime and should return an adjusted value.Only called forawaredatetimes, including those created bylocalizeDateDate(formats,localize=None,astz=None)Validate that the input is a Pythondate.Supports the following input values:date: passthroughdatetime: takes the.date()partstring: parses (seeDateTime)fromdatetimeimportdatefromgoodimportSchema,Dateschema=Schema(Date('%Y-%m-%d'))schema('2014-09-06')#-> datetime.date(2014, 9, 6)schema(date(2014,9,6))#-> datetime.date(2014, 9, 6)schema('2014')#-> Invalid: Invalid date format, expected Date, got 2014.Arguments:TimeTime(formats,localize=None,astz=None)Validate that the input is a Pythontime.Supports the following input values:time: passthroughdatetime: takes the.timetz()partstring: parses (seeDateTime)Sincetimeis subject to timezone problems,
make sure you've read the notes in the relevant section ofDateTimedocs.Arguments:FilesIsFileIsFile()Verify that the file exists.fromgoodimportSchema,IsFileschema=Schema(IsFile())schema('/etc/hosts')#-> '/etc/hosts'schema('/etc')#-> Invalid: is not a file: expected Existing file path, got /etcIsDirIsDir()Verify that the directory exists.PathExistsPathExists()Verify that the path exists.