checktypes

Library for creating utility classes giving a nice abstraction for type checking and data validation


License
GPL-3.0
Install
pip install checktypes==0.5.post1

Documentation

CheckTypes package

Library for creating utility classes giving a nice abstraction for type checking and data validation.

Basic examples

Creation

Object Oriented API

Choose a base class alongside CheckType to inherit from and define a predicate() staticmethod or classmethod

>>> from checktypes import CheckType
>>> class PositiveInteger(int, CheckType):
...     "'int' > 0"
...     @staticmethod
...     def predicate(n):
...         return n > 0
...
>>> class HumanAge(int, CheckType):
...     "'int' between 0 and 125"
...     minval = 0
...     maxval = 125
...     @classmethod
...     def predicate(cls, val):
...         return cls.minval <= val <= cls.maxval
...

Alternatively, you can also use lambdas for shorter class definitions.

>>> class PositiveInteger(int, CheckType):
...     "'int' > 0"
...     predicate = lambda n: n > 0
...
>>> class HumanAge(int, CheckType):
...     "'int' between 0 and 125"
...     minval = 0
...     maxval = 125
...     predicate = classmethod(lambda cls, n: cls.minval <= n <= cls.maxval)
...

Functional API

Another way is to use the checktype() factory.

>>> from checktypes import checktype
>>> PositiveInteger = checktype('PositiveInteger', int, lambda n: n > 0, "'int' > 0")
>>> HumanAge = checktype(
...     'HumanAge', int, doc="'int' between 0 and 125", minval=0, maxval=125,
...     predicate=classmethod(lambda cls, n: cls.minval <= n < cls.maxval)
... )
...

Usage

isinstance() overload

>>> isinstance(1, PositiveInteger)
True
>>> isinstance(-1, PositiveInteger)
False
>>> isinstance('a', PositiveInteger)
False

validate() classmethod

>>> PositiveInteger.validate(1)  # No output => the value is a valid one
>>> PositiveInteger.validate(-1)
Traceback (most recent call last):
 ...
ValueError: expected 'PositiveInteger' ('int' > 0) but got -1
>>> PositiveInteger.validate('a')
Traceback (most recent call last):
 ...
TypeError: expected 'PositiveInteger' ('int' > 0) but got 'str'

register() classmethod

>>> isinstance(0, PositiveInteger)
False
>>> PositiveInteger.validate(0)
Traceback (most recent call last):
 ...
ValueError: expected 'PositiveInteger' ('int' > 0) but got 0
>>> PositiveInteger.register(0)  # Now let pass 0
>>> isinstance(0, PositiveInteger)
True
>>> PositiveInteger.validate(0)

as_descriptor() classmethod

>>> class Circle:
...     radius = PositiveInteger.as_descriptor()
...
>>> c = Circle()
>>> c.radius = 1
>>> c.radius = -1
Traceback (most recent call last):
 ...
ValueError: expected 'PositiveInteger' ('int' > 0) but got -1 for 'radius' attribute of 'Circle' object
>>> c.radius = 'a'
Traceback (most recent call last):
 ...
TypeError: expected 'PositiveInteger' ('int' > 0) but got 'str' for 'radius' attribute of 'Circle' object

checktyped decorator with type hints (3.6+ style)

>>> from checktypes import checktyped
>>> @checktyped
... class Point2D:
...     x: float
...     y: float
...
>>> p = Point2D()
>>> p.x = 0.0
>>> p.y = 1.0
>>> p.x = 'a'
Traceback (most recent call last):
 ...
TypeError: expected 'float' but got 'str' for 'x' attribute of 'Point2D' object

Instantiation

By concept CheckTypes are not originally meant to be instantiated. But since it's a common task to cast a value into another type, support have been added to make the constructor return a value with the same rules as a standard class in python except three things:

1 - The returned value will never be an instance of the class but an instance of one a its bases.

>>> PositiveInteger(1)
1
>>> n = PositiveInteger(1)
>>> print(n, type(n), sep=': ')
1: <class 'int'>

2 - If the value doesn't satisfy the isinstance() check, a ValueError will be raise.

>>> PositiveInteger(-1)
Traceback (most recent call last):
 ...
ValueError: -1 cannot be interpreted as a 'PositiveInteger' ('int' > 0)

3 - __init__() and __new__() are ignored.

>>> class MyInt(int, CheckType):
...     def __new__(cls, x):
...         return 'unexpected thing'
...     def __init__(self, x):
...         self.my_attr = 'some value'
...
>>> x = MyInt(1)
>>> x
1
>>> x.my_attr
Traceback (most recent call last):
 ...
AttributeError: 'int' object has no attribute 'my_attr'


Still, two class attributes can be supplied to add support for better instantiation:

1 - default

It provides a value to be returned if the class is called without argument.
One of its merit: it fixes the problem of unfit default value.

>>> class NegativeInteger(int, CheckType):
...     default = -1
...     predicate = lambda n: n < 0
...
>>> NegativeInteger()
-1
>>> del NegativeInteger.default
>>> NegativeInteger()  # int() -> 0
Traceback (most recent call last):
 ...
ValueError: 0 cannot be interpreted as a 'NegativeInteger'

2 - factory()

It is designed to be a callable with the responsibility of returning the new object.
Specially useful when inheriting from an ABC.

>>> from collections.abc import Sized
>>> class ThreePlace(Sized, CheckType):
...     factory = tuple
...     predicate = lambda s: len(s) == 3
...
>>> ThreePlace(range(1, 4))
(1, 2, 3)
>>> ThreePlace([4, 5, 6])
(4, 5, 6)
>>> ThreePlace('789')
('7', '8', '9')

Be aware that the returned value will still be checked.

>>> def badfactory(*args, **kwarg):
...     return 'bad value'
...
>>> ThreePlace.factory = badfactory
>>> ThreePlace((1, 2, 3))
Traceback (most recent call last):
 ...
ValueError: 'bad value' cannot be interpreted as a 'ThreePlace'
>>> del ThreePlace.factory
>>> ThreePlace.default = 0
>>> ThreePlace()
Traceback (most recent call last):
 ...
TypeError: 'int' object cannot be interpreted as a 'ThreePlace'

For other examples, see Recipes.md.