Default normalise helpers¶
spec_base is the core of the norms package and implements many specification
classes based off the Spec
class.
A specification is an object with a normalise
method and is used to validate
and transform data.
The following items are all found under delfick_project.norms.sb
. For example:
from delfick_project.norms import sb, Meta
class double_spec(sb.Spec):
def normalise_filled(self, meta, val):
val = sb.integer_spec().normalise(meta, val)
return val * 2
lst = sb.listof(double_spec()).normalise(Meta.empty(), ["3", 4, 5])
assert lst == [6, 8, 10]
-
class
delfick_project.norms.spec_base.
NotSpecified
¶ Tell the difference between None and not specified
-
delfick_project.norms.spec_base.
apply_validators
(meta, val, validators, chain_value=True)¶ Apply a number of validators to a value.
- chain_value
- Sets whether to pass the validated value into the next validator or whether to use the original value each time
All validators are tried and errors are collected.
If any fails, an error is raised, otherwise the value is returned.
Where value is the original value, or the result of the last validator depending on
chain_value
.
-
class
delfick_project.norms.spec_base.
Spec
(*pargs, **kwargs)¶ Default shape for a spec (specification, not test!)
-
__init__
(*pargs, **kwargs)¶ passes all
*args
and**kwargs
to aself.setup
if that exists on the class
-
normalise
(meta, val)¶ Calls one of the following functions on the class, choosing in this order:
- normalise_either(meta, val)
If defined, then this is meant to work with both a specified value as well as
NotSpecified
.If this function returns
NotSpecified
, then we continue looking for a function to handle this value.- normalise_empty(meta)
- Called if the value is
NotSpecified
. - default(meta)
- Called if the value is
NotSpecified
. - return NotSpecified
- If the value is
NotSpecified
and the above don’t return, then the value is returned as is. - normalise_filled(meta, val)
- The value is not
NotSpecified
If none of those options are defined, then an error is raised complaining that we couldn’t work out what to do with the value.
Note that any validation errors should be an subclass of
BadSpec
. This is because the default specs only catch such exceptions. Anything else is assumed to be a ProgrammerError and worthy of a traceback.
-
fake_filled
(meta, with_non_defaulted=False)¶ If a
fake
method is on the class, we returnfake(meta, with_non_defaulted)
otherwise, if a
default
method is on the class, we returndefault(meta)
otherwise we just return
NotSpecified
The idea is that a Spec is an object with a
normalise
method that takes in two objects:meta
andval
.meta
Should be an instance of
Meta
and is used to indicate where we are. This should be passed to any children specifications.For example, if we are normalising a dictionary where a child specification is used on a particular value at a key called
a_key
, we should do something similar to:child_spec().normalise(meta.at("a_key"), val["a_key"])
val
Should be the value we are normalising. The normalising process should validate and transform the value to be what we are specifying.
For example sb.listof(sb.string_spec()) will transform a single string to be a list of one string.
When you create a subclass of
Spec
you either implement one of thenormalise_*
methods ornormalise
itself.-
pass_through_spec¶
- Usage
pass_through_spec().normalise(meta, val)Will not touch the value in any way and just return it.
always_same_spec¶
- Usage
always_same_spec(result).normalise(meta, val)Will ignore value and just return
result
.
dictionary_spec¶
- Usage
dictionary_spec().normalise(meta, val)Will normalise
NotSpecified
into{}
Specified values are valid if
type(val) is dict
orval.is_dict() is True
. If either those conditions are true, then the dictionary is returned as is.
dictof¶
- Usage
dictof(name_spec, value_spec).normalise(meta, val) # or dictof(name_spec, value_spec, nested=True).normalise(meta, val)This will first use
dictionary_spec
logic on the value to ensure we are normalising a dictionary.It will then use
name_spec
andvalue_spec
on the items in the value to produce a resulting dictionary.For example if we have
{"a": 1, "b": 2}
, using dictof is equivalent to:{ name_spec.normalise(meta.at("a"), "a"): value_spec.normalise(meta.at("a"), 1) , name_spec.normalise(meta.at("b"), "b"): value_spec.normalise(meta.at("b"), 2) }This specification will also do the same to any
dictionary
values it has ifnested
is set toTrue
(defaults toFalse
)It will also collect any errors and raise a collection of all errors it comes across.
tupleof¶
- Usage
tupleof(spec).normalise(meta, val)This specification will transform
NotSpecified
into()
If we don’t have a tuple, we turn the value into a tuple of that value. Except for lists which are turned in a tuple.
The resulting tuple of items is returned.
listof¶
- Usage
listof(spec).normalise(meta, val) # or listof(spec, expect=typ).normalise(meta, val)This specification will transform
NotSpecified
into[]
If we don’t have a list, we turn the value into a list of that value.
If
expect
is specified, any item already passingisinstance(item, expect)
is left alone, otherwisespec
is used to normalise the value.The resulting list of items is returned.
set_options¶
- Usage
set_options(<key1>=<spec1>, ..., <keyn>=<specn>).normalise(meta, val) # For example set_options(key_1=integer_spec(), key_2=string_spec()).normalise(meta, val)This specification transforms
NotSpecified
into{}
.Specified values are validated using
dictionary_spec
and then for each keyword argument we use either that key in theval
orNotspecified
and normalise it with thespec
for that key.Finally, we assemble the result from all these keys in a dictionary and return that.
Errors are collected and raised in a group.
Extra keys in
val
are ignored.
defaulted¶
- Usage
defaulted(spec, dflt).normalise(meta, val)This specification will return
dflt
ifval
isNotSpecified
.Otherwise, it merely proxies
spec
and doesspec.normalise(meta, val)
required¶
- Usage
required(spec).normalise(meta, val)This specification will raise an error if
val
isNotSpecified
.Otherwise, it merely proxies
spec
and doesspec.normalise(meta, val)
boolean¶
- Usage
boolean().normalise(meta, val)This complains if the value is not
isintance(val, bool)
.Otherwise it just returns the
val
.Note
This specification does not handle
NotSpecified
. This is a deliberate decision. Use defaulted(boolean(), <dflt>) if you want to handle that.
directory_spec¶
- Usage
directory_spec().normalise(meta, val) # or directory_spec(spec).normalise(meta, val)This specification will first normalise
val
withspec
ifspec
is specified.It then makes sure that
val
is a string, exists, and is a directory.If it isn’t, an error is raised, otherwise the
val
is returned.
filename_spec¶
- Usage
directory_spec().normalise(meta, val) # or filename_spec(spec).normalise(meta, val)This specification will first normalise
val
withspec
ifspec
is specified.It then makes sure that
val
is a string, exists, and is a file.If it isn’t, an error is raised, otherwise the
val
is returned.
file_spec¶
- Usage
file_spec().normalise(meta, val)This will complain if
val
is not a file object, otherwise it just returnsval
.
string_spec¶
- Usage
string_spec().normalise(meta, val)This transforms
NotSpecified
into""
If
val
is specified, it will complain if notisinstance(val, str)
, otherwise it just returnsval
.
integer_spec¶
- Usage
integer_spec().normalise(meta, val)This will complain if
val
is not an integer, unless it hasisdigit
and this function returnsTrue
.We return
int(val)
regardless.Note
This specification does not handle
NotSpecified
. This is a deliberate decision. Use defaulted(integer_spec(), <dflt>) if you want to handle that.
float_spec¶
- Usage
float_spec().normalise(meta, val)If the
val
is not abool
then we dofloat(val)
and return the result.Otherwise, or if that fails, an error is raised.
string_or_int_as_string_spec¶
- Usage
string_or_int_as_string_spec().normalise(meta, val)This transforms
NotSpecified
into""
If the
val
is not an integer or string, it will complain, otherwise it returnsstr(val)
.
valid_string_spec¶
- Usage
valid_string_spec(validator1, ..., validatorn).normalise(meta, val)This takes in a number of validator specifications and applies them to
val
after passing throughvalid_string_spec
logic.Validators are just objects with
normalise
methods that happen to raise errors and return theval
as is.If none of the validators raise an error, the original
val
is returned.
integer_choice_spec¶
- Usage
integer_choice_spec([1, 2, 3]).normalise(meta, val) # or integer_choice_spec([1, 2, 3], reason="Choose one of the first three numbers!").normalise(meta, val)This absurdly specific specification will make sure
val
is an integer before making sure it’s one of thechoices
that are provided.It defaults to complaining
Expected one of the available choices
unless you providereason
, which it will use instead if it doesn’t match one of the choices.
string_choice_spec¶
- Usage
string_choice_spec(["a", "b", "c"]).normalise(meta, val) # or string_choice_spec(["a", "b", "c"], reason="Choose one of the first three characters in the alphabet!").normalise(meta, val)This absurdly specific specification will make sure
val
is a string before making sure it’s one of thechoices
that are provided.It defaults to complaining
Expected one of the available choices
unless you providereason
, which it will use instead if it doesn’t match one of the choices.
create_spec¶
- Usage
create_spec( kls , validator1, ..., validatorn , key1=spec1, ..., keyn=specn ).normalise(meta, val)This specification will return
val
as is if it’s already an instance ofkls
.Otherwise, it will run
val
through all thevalidator``s before using the ``key
->``spec`` keyword arguments in aset_options
specification to create the arguments used to instantiate an instance ofkls
.
or_spec¶
- Usage
or_spec(spec1, ..., specn).normalise(meta, val)This will keep trying
spec.normalise(meta, val)
until it finds one that doesn’t raise aBadSpec
error.If it can’t find one, then it raises all the errors as a group.
match_spec¶
- Usage
match_spec((typ1, spec1), ..., (typn, specn)).normalise(meta, val) # or match_spec((typ1, spec1), ..., (typn, specn), fallback=fspec).normalise(meta, val)This will find the
spec
associated with the firsttyp
that succeedsisinstance(val, typ)
.Note
If
spec
is callable, we dospec().normalise(meta, val)
.If fallback is specified and none of the
typ``s match the ``val
thenfpsec
is used as thespec
. It is also called first if it’s a callable object.If we can’t find a match for the
val
, an error is raised.
and_spec¶
- Usage
and_spec(spec1, ..., specn).normalise(meta, val)This will do
val = spec.normalise(meta, val)
for eachspec
that is provided and returns the finalval
.If any of the
spec
fail, then an error is raised.
optional_spec¶
- Usage
optional_spec(spec).normalise(meta, val)This will return
NotSpecified
if theval
isNotSpecified
.Otherwise it merely acts as a proxy for
spec
.
dict_from_bool_spec¶
- Usage
dict_from_bool_spec(dict_maker, spec).normalise(meta, val)If
val
isNotSpecified
then we dospec.normalise(meta, {})
If
val
is a boolean, we first doval = dict_maker(meta, val)
and then dospec.normalise(meta, val)
to return the value.Example:
A good example is setting enabled on a dictionary:
spec = dict_from_bool_spec(lambda b: {"enabled": b}, set_options(enabled=boolean())) spec.normalise(meta, False) == {"enabled": False} spec.normalise(meta, {"enabled": True}) == {"enabled": True}
formatted¶
- Usage
formatted(spec, formatter).normalise(meta, val) # or formatted(spec, formatter, expected_type=typ).normalise(meta, val) # or formatted(spec, formatter, after_format=some_spec()).normlise(meta, val)This specification is a bit special and is designed to be used with
MergedOptionStringFormatter
from theoption_merge
library (http://option-merge.readthedocs.org/en/latest/docs/api/formatter.html).The idea is that
meta.everything
is an instance ofMergedOptions
and it will create a new instance ofmeta.everything.__class__
usingmeta.everything.converters
andmeta.everything.dont_prefix
if they exist. Note that this should work with normal dictionaries as well.We then update our copy of
meta.everything
withmeta.key_names()
and create an instance offormatter
using this copy of themeta.everything
,meta.path
andspec.normalise(meta, val)
as the value.We call
format
on theformatter
instance, check that it’s an instance ofexpected_type
if that has been specified.Once we have our formatted value, we normalise it with after_format if that was specified.
And finally, return a value!
many_format¶
- Usage
many_format(spec, formatter).normalise(meta, val) # or many_format(spec, formatter, expected_type=typ).normalise(meta, val)This is a fun specification!
It essentially does
formatted(spec, formatter, expected_type=typ).normalise(meta, val)
until the result doesn’t change anymore.Before doing the same thing on
f"{{{val}}}"
Example:
Let’s say we’re at
images.my_image.persistence.image_name
in the configuration.This means
{_key_name_2}
(which is frommeta.key_names()
) is equal tomy_image
.many_format(overridden("images.{_key_name_2}.image_name"), formatter=MergedOptionStringFormatter) # Is equivalent to formatted(overridden("{images.my_image.image_name}"), formatter=MergedOptionStringFormatter)This essentially means we can format a key in the options using other keys from the options!
overridden¶
- Usage
overridden(value).normalise(meta, val)This will return
value
regardless of whatval
is!
any_spec¶
- Usage
any_spec().normalise(meta, val)Will return
val
regardless of whatval
is.
container_spec¶
- Usage
container_spec(kls, spec).normalise(meta, val)This will apply
spec.normalise(meta, val)
and callkls
with the result of that as the one argument.Note
if the
val
is alreadyisinstance(val, kls)
then it will just returnval
.
delayed¶
- Usage
delayed(spec).normalise(meta, val)This returns a function that when called will do
spec.normalise(meta, val)
typed¶
- Usage
typed(kls).normalise(meta, val)This will return the value as is as long as it’s isinstance of
kls
Otherwise it complains that it’s the wrong type
has¶
- Usage
has(prop1, prop2, ..., propn).normalise(meta, val)This will complain if the value does not have any of the specified properties (using hasattr)
tuple_spec¶
- Usage
tuple_spec(spec1, spec2, ..., specn).normalise(meta, val)Will complain if the value is not a tuple or doesn’t have the same number of items as specified specs.
Will complain if any of the specs fail for their respective part of val.
Returns the result of running all the values through the specs as a tuple.
none_spec¶
- Usage
none_spec().normalise(meta, val)Will complain if the value is not None. Otherwise returns None.
Defaults to None.
many_item_formatted_spec¶
- Usage
class FinalKls(dictobj): fields = ["one", "two", ("three", None)] class my_special_spec(many_item_formatted_spec): specs = [integer_spec(), string_spec()] def create_result(self, one, two, three, meta, val, dividers): if three is NotSpecified: return FinalKls(one, two) else: return FinalKls(one, two, three) # The rest of the options are optional creates = FinalKls value_name = "special" seperators = "^" optional_specs = [boolean()] spec = my_special_spec() spec.normalise(meta, "1^tree") == FinalKls(1, "tree") spec.normalise(meta, [1, "tree"]) == FinalKls(1, "tree") spec.normalise(meta, [1, tree, False]) == FinalKls(1, "tree", False)We can also define modification hooks for each part of the spec:
class my_special_spec(many_item_formatted_spec): specs = [integer_spec(), integer_spec(), integer_spec()] def spec_wrapper_2(self, spec, one, two, meta, val, dividers): return defaulted(spec, one + two) def determine_2(self, meta, val): return 42 def alter_2(self, one, meta, original_two, val): if one < 10: return original_two else: return original_two * 10 def alter_3(self, one, two, meta, original_three, val): if two < 100: return original_three else: return original_three * 100 def create_result(self, one, two, three, meta, val, dividers): return FinalKls(one, two, three)A spec for something that is many items Either a list or a string split by “:”
If it’s a string it will split by ‘:’ Otherwise if it’s a list, then it will use as is and will complain if it has two many values
It will use determine_<num> on any value that is still NotSpecified after splitting the val.
And will use alter_<num> on all values after they have been formatted.
Where <num> is 1 indexed index of the value in the spec specifications.
Finally, create_result is called at the end to create the final result from the determined/formatted/altered values.