Reference

Quick schema validation.

nil = <object object>

Used to signify nonexistance.

exception Error(code, *info, message=None)

Thrown when something goes wrong.

Variables:
  • code (str) – Means of identification.
  • info (list[str]) – Additional details used.
  • chain (gen) – Yields all contributing errors.
show(use=<function Error.<lambda>>, sep='\n')

Get simple json-friendly info about this error family.

Parameters:
  • use (func) – Used on every (code, info) for each back-error and should return str.
  • sep (str) – Used to join all parts together.
class Op

Bases: tuple

Represents a collection of operatable values.

class Nex

Bases: Op

Represents the OR operator.

Values will be checked in order. If none pass, the last error is raised.

First data passing will be used for tracing.

>>> def less(value):
>>>     return value < 5:
>>> def more(value):
>>>     return value > 9
>>> fig = Nex(int, less, more)
>>> check(fig, 12)

The above will pass, since 12 is greater than 9.

Or

Alias of Nex.

class And

Bases: Op

Represents the AND operator.

Values will be checked in order. If one fails, its error is raised.

Last data passing will be used for tracing.

>>> def less(value):
>>>     return value < 5:
>>> def more(value):
>>>     return value > 9
>>> fig = And(int, less, more)
>>> check(fig, 12)

The above will fail, since 12 is not less than 5.

class Opt(value)

Signals an optional value.

>>> fig = {Opt('one'): int, 'two': int}
>>> check(fig, {'two': 5})

The above will pass since "one" is not required but "two" is.

class Con(change, figure)

Signals a conversion to the data before checking.

Data will not be used for tracing.

>>> def less(value):
>>>     return value < 8
>>> fig = (str, Con(len, less))
>>> check(fig, 'ganglioneuralgia')

The above will fail since the length of… that is greater than 8.

class Rou(figure, success, failure=<object object>)

Routes validation according to a condition.

Data passing will be used for tracing.

>>> fig = And(
>>>     str,
>>>     If(
>>>         lambda data: '@' in data,
>>>         email_fig, # true
>>>         Con(int, phone_fig) # false
>>>     )
>>> )
>>> check(fig, 'admin@domain.com')
>>> check(fig, '0123456789')
If

Alias of Rou.

class Not(figure)

Represents the NOT operator.

Data will be used for tracing.

fig = Not(And(str, Con(len, lambda v: v > 5)))
check(fig, 'pass1234')
class Exc(figure, exceptions)

Fail when exceptions are raised.

Data will be used for tracing.

fig = Exc(int, ValueError)
check(figure, data, auto=False, extra=[])

Validates data against the figure and returns a compliant copy.

Parameters:
  • figure (any) – Some object or class to validate against.
  • data (any) – Some object or class to validate.
  • auto (bool) – Whether to validate types.
  • extra (list[func]) – Called with figure and should return :var:`.nil` or a figure.
trace(figure, data, auto=False, extra=[])

Alias of check.

wrap(figure, *parts, **kwargs)

Use parts when an error is raised against this figure.

kwargs gets passed to check() automatically.

>>> fig = wrap(
>>>     shucks.range(4, math.inf, left = False),
>>>     'cannot be over 4'
>>> )
>>> data = ...
>>> check(fig, data)

Errors

Type fails raise:

Error('type', expected_cls, given_cls)

Object equality fails raise:

Error('object', expected_val, given_val)

Array value check fails raise:

Error('index', index) from source_error

Arrays that are too small raise:

Error('small', expected_size, actual_size)

Arrays that have at least one additional value raise:

Error('large', exceeded_size)

Note

Array checking is done in an iterator-exhausting fashion.

Upon finishing, one more item is attempted to be generated for this error.

Missing mapping keys raise:

Error('key', expected_key)

Mapping value check fails raise:

Error('value', current_key) from source_error

Callables that don’t return True raise:

Error('call', used_func, current_data)

Validators

Some ready-made validators exist to make your life easier:

range(a, b=None, left=True, right=True)

Check whether the value is between the lower and upper bounds.

Parameters:
  • a (float) – Lower or upper bound.
  • b (float) – Upper bound.
  • left (bool) – Whether lower bound is inclusive.
  • right (bool) – Whether upper bound is inclusive.
>>> figure = range(5.5, left = False) # (0, 5.5]
>>> check(figure, 0) # fail, not included
contain(store, white=True)

Check the membership of the value against the store.

Parameters:
>>> import string
>>> figure = contain(string.ascii_lowercase)
>>> check(figure, 'H') # fail, capital

Examples

All snippets assume:

import shucks as s

Basic

Check if the data is a int instance:

int

Check if the data is 4.5:

4.5

Check if the data is 4.5 or 8:

s.Or(4.5, 8)

Check if the data is a str instance and 'hello':

s.And(str, 'hello')

Check if the data is not str and 'hello':

s.Not(s.And(str, 'hello'))

Check if the data is str and casting to abs does not raise TypeError:

s.And(str, s.Exc(abs, TypeError))

Check if the length of the data is 10:

s.Con(len, 10)

Check if the data is between incl 1 and excl 10:

s.range(1, 10, right = False)

Check if the data is a str instance and its length between incl 1 and incl 10:

s.And(str, s.Con(len, s.range(1, 10)))

Check further only if str:

s.If(str, s.Con(len, s.range(1, 10)))

Check if 10 characters when str, or convert and check otherwise.

_c_lr = s.Con(len, s.range(1, 10))

s.If(str, _c_lr, s.Con(str, _c_lr))

Raise an error upon failure:

s.wrap(s.Con(len, s.range(1, 10)), 'phone', 'no more than 10 digits required')

Check if the data is even:

even = lambda data: not data % 2

Note

Refer to the end of Errors for info about failing callables.

Array

Check if the data has two values, both being 0 or 1:

[s.Or(0, 1), s.Or(0, 1)]

Check if the data has at least one value of 0 or 1:

[s.Or(0, 1), ...]

Check if the data has any amount of 0 or 1:

s.Or([], [s.Or(0, 1), ...])

Tip

Since our schema will probably remain static, using tuple instead of list would have the same result and save us some memory.

Check if the data is a list instance with any amount of 0 or 1:

s.And(list, s.Or((), (s.Or(0, 1), ...)))

Note

To enable auto type checks, simply pass auto = True to check().

Check if the data has at least one of 0 or 1, followed by one value of 2:

[s.Or(0, 1), ..., 2]

Check if the data is empty or has at at least one of 0 or 1, followed by at least one value of 2:

s.Or([], [s.Or(0, 1), ..., 2, ...])

Warning

This does not check if the data has any amount of 0 or 1 followed by any amount of 2

Tip

To get the same effect as with s.Or([], [s.Or(0, 1), ...]), checking for any amount for both s.Or(0, 1) and 2, have to do:

s.Or([], [s.Or(0, 1), ...], [s.Or(0, 1), ..., 2, ...])

Or offers a way to eliminate this redundancy, the above is equal to:

s.Or([s.Or(0, 1), ..., 2, ...], any = True)

Nothing else other than one array should be used with with the any option.

To emulate using more arguments for Or with any, nest them:

s.Or(list, s.Or([s.Or(0, 1), ..., 2, ...], any = True))

Check if the data is a tuple or list instance, with any amount of 0 or 1 followed by any amount of 2 and ending with a 3:

s.And(s.Or(tuple, list), s.Or((), s.Or((s.Or(0, 1), ..., 2, ..., 3), any = True)))

Check if every line in a file starts with _:

def read(name):
  with open(name) as file:
    data = file.read().splitlines()
  return data

s.And(
  str,
  s.Con(
    read,
    s.wrap(
      s.Or((lambda line: line.startswith('_'), ...), any = True),
      'missing _'
    )
  )
)

Mapping

Check if the data has at least one foo key against bar:

{'foo': 'bar'}

Check if the data has at most one foo key against bar:

{s.Opt('foo'): 'bar'}

Note

The whole “at least” and “at most” terminology might be confusing in this section.

Consider “required” and “optional” respectively.

Check if the data has a required tic key against tac or toe:

{'tic': s.Or('tac', 'toe')}

Warning

Using Ops or Sigs other than Opt for keys will lead to unexpected behaviour.

Contained data is __getitem__’d from the root data, not checked sequentially for existence.

Tip

To better validate key-value pairs, do:

s.Con(dict.items, (('key', 'value'), ...))

Extra

Consider the follow classes:

class MyClass0:
   def __init__(self, v):
      self.v = v

class MyClass1:
   def __init__(self, n):
      self.n = n

Create functions (d_) that conditionally return checkers (c_):

def c_0(figure, obj):
   shucks.check(int, obj.v)

def c_0(figure, obj):
   shucks.check(str, obj.n)

def d_0(figure):
   if isinstance(figure, MyClass0):
      return c_0
   if isinstance(figure, MyClass1):
      return c_1

determinators = [d_0]

Now simply use extra = determinators when your data includes objects of such classes.

Note

extra accepts a list for adding and removing determinators accordingly.

Practices

There’s a few things you can do to make validation more understandable.

For example, when raising Error, either manually or through wrap(), you may want to include some sort of “code” as the first argument, and then assets that can be used to form an explanation, rather than the explanation itself.

So, having these:

primes = {11, 13, 17, 19, 23, 29, 31, 37}
length = 3
subfig = s.Con(primes.intersection, s.Con(len, 3))

So instead of doing something like this:

s.wrap(subfig, '3 prime numbers over 10 and under 40 required')

Strive to do something like this:

s.wrap(subfig, 'primes', primes, length)

When this fails, the user will programatically be able to handle the error and make a more educated deduction about what went wrong, and better yet, how to fix it.

Another good practice that’s already visible is wrap()ing all Cons, so as to better communicate the transformations that occurred during validation.

For example, consider the following figure:

s.And(int, s.Con(str, s.Con(len, s.range(8, 64))))

The ideal data should be an int, and the innermost check that happens is a range(). However, that range is for the amount of digits the number has, not the number itself.

Not knowing the conversions that took place can lead to a very confusing error if this were to be checked against, let’s say, 42 as it’s definitely between 8 and 64, but an error is raised saying that it’s not.

So a better way of formulating this figure would be:

s.And(int, s.wrap(s.Con(str, s.Con(len, s.range(8, 64))), 'digit amount'))

Notice how not both Cons were wrap()ed, as they describe consecutive conversions that cannot easily be described separately.