# Copyright 2018 Max Shinn <max@maxshinnpotential.com>
#
# This file is part of Paranoid Scientist, and is available under the
# MIT license. Please see LICENSE.txt in the root directory for more
# information.
__all__ = ['TypeFactory', 'Type', 'Constant', 'Unchecked', 'Generic',
'InitGeneric', 'Self', 'Nothing', 'Function', 'Boolean',
'And', 'Or', 'Not', 'Maybe', 'Void']
from ..exceptions import VerifyError, NoGeneratorError, InvalidTypeError
import inspect
[docs]def TypeFactory(v):
"""Ensure `v` is a valid Type.
This function is used to convert user-specified types into
internal types for the verification engine. It allows Type
subclasses, Type subclass instances, Python type, and user-defined
classes to be passed. Returns an instance of the type of `v`.
Users should never access this function directly.
"""
if v is None:
return Nothing()
elif issubclass(type(v), Type):
return v
elif isinstance(v, type) and issubclass(v, Type):
return v()
elif issubclass(type(v), type):
return Generic(v)
else:
raise InvalidTypeError("Invalid type %s" % v)
class _MetaType(type):
def __repr__(cls):
return cls.__name__
[docs]class Type(metaclass=_MetaType):
"""The base Type, from which all variable types should inherit.
What is a Type? While "types" can include standard types built
into the programming language (e.g. int, float, string), they can
also be more flexible. For example, you can have a "natural
numbers" type, or a "range" type, or even a "file path" type.
All types must inherit from this class. They must define the
"test" and "generate" functions.
"""
def __init__(self, *args, **kwargs):
# Create the string representation
pargs = [repr(v) for v in args]
kargs = [k+"="+repr(v) for k,v in kwargs.items()]
allargs = pargs+kargs
self._repr = self.__class__.__name__
if allargs:
self._repr += "(%s)" % (", ".join(pargs+kargs))
super().__init__()
def __repr__(self):
return self._repr
[docs] def test(self, v):
"""Check whether `v` is a valid value of this type. Throws an
assertion error if `v` is not a valid value.
"""
pass
[docs] def generate(self):
"""Generate a list of values of this type."""
raise NotImplementedError("Please subclass Type")
def __contains__(self, v):
try:
self.test(v)
except AssertionError:
return False
else:
return True
[docs]class Constant(Type):
"""Only one valid value, which is passed at initialization"""
def __init__(self, const):
super().__init__(const=const)
assert const is not None, "None cannot be a constant"
self.const = const
def __repr__(self):
return "Constant(%s)" % repr(self.const)
def test(self, v):
super().test(v)
assert self.const == v, "Invalid constant"
def generate(self):
yield self.const
[docs]class Unchecked(Type):
"""Use type `typ` but do not check it."""
def __init__(self, typ=None):
if typ is not None:
self.typ = TypeFactory(typ)
super().__init__(self.typ)
else:
self.typ = None
super().__init__()
def generate(self):
if self.typ is not None:
yield from self.typ.generate()
[docs]class Generic(Type):
"""Wraps Python classes to turn them into Types.
Classes may optionally contain the "_test_(v)" or "_generate()"
static methods; adding these two functions gives them the same
power as traditional Types. "_test(v)" should check if `v` is a
valid member of the class using assert statements only, and
"_generate()" should yield a finite number of instances of the class.
"""
def __init__(self, typ):
super().__init__(typ=typ)
assert isinstance(typ, type)
assert not isinstance(typ, Type), "Types don't need to be wrapped"
self.type = typ
def test(self, v):
assert isinstance(v, self.type)
type_and_parents = reversed(self.type.mro()[:-1]) # -1 removes <class 'object'>
for t in type_and_parents:
if hasattr(t, "_test") and callable(t._test):
t._test(v)
def __repr__(self):
return "Generic(%s)" % self.type
def generate(self):
if hasattr(self.type, "_generate") and callable(self.type._generate):
yield from self.type._generate()
else:
raise NoGeneratorError("Please define a _generate() function in "
"class %s." % self.type.__name__)
[docs]class InitGeneric(Type):
"""For the self argument passed to __init__. Should not be used directly, use Self instead.
Before a class is initialized (i.e. __init__ is called), it may
not be valid according to its _test method. Likewise, passing a
fully initialized class value to __init__ through the self
parameter may cause problems, as the self parameter for __init__
is the output of the __new__ method. Thus, the __init__ function
must be handled separately. This tests only object identity, and
generates types based on the __new__ method. This may fail for
"""
def __init__(self, typ):
super().__init__(typ)
assert isinstance(typ, type)
assert not isinstance(typ, Type), "Types don't need to be wrapped"
self.type = typ
def test(self, v):
assert isinstance(v, self.type)
def generate(self):
# We can't generate if the __new__ method takes arguments
nargs = len(inspect.getfullargspec(self.type.__new__).args)
if nargs == 1:
# __new__ automatically calls __init__, so set __init__ to
# an empty function to temporarily avoid calling it
init = self.type.__init__
self.type.__init__ = lambda *args, **kwargs: None
obj = self.type.__new__(self.type)
self.type.__init__ = init
yield obj
[docs]class Self(Type):
"""Used only as a placeholder for methods with a 'self' argument."""
def test(self, v):
raise VerifyError("Invalid use of the Self type. (Did you forget to use @paranoidclass?)")
def generate(self):
raise VerifyError("Invalid use of the Self type. (Did you forget to use @paranoidclass?)")
[docs]class Nothing(Type):
"""The None type."""
def test(self, v):
assert v is None
def generate(self):
yield None
[docs]class Void(Type):
"""Always fails."""
def test(self, v):
assert False
def generate(self):
raise NoGeneratorError
# TODO expand this to define argument/return types
[docs]class Function(Type):
"""Any function."""
def test(self, v):
super().test(v)
assert callable(v), "Not a function"
def generate(self):
raise NoGeneratorError
[docs]class Boolean(Type):
"""True or False"""
def test(self, v):
super().test(v)
assert v in [True, False], "Not a boolean"
def generate(self):
yield True
yield False
[docs]class And(Type):
"""Conforms to all of the given types.
Any number of Types can be passed to And. The And of these types
is the logical AND of them, i.e. a value must conform to each of
the types.
"""
def __init__(self, *types):
self.types = [TypeFactory(a) for a in types]
super().__init__(*self.types)
def test(self, v):
for t in self.types:
t.test(v)
def generate(self):
all_generated = [e for t in self.types for e in t.generate() or []]
valid_generated = []
for g in all_generated:
try:
for t in self.types:
t.test(g)
except AssertionError:
continue
else:
yield g
[docs]class Or(Type):
"""Conforms to any of the given types.
Any number of Types can be passed to Or. The Or of these types is
the logical OR of them, i.e. a value must conform to at least one
the types.
"""
def __init__(self, *types):
self.types = [TypeFactory(a) for a in types]
super().__init__(*self.types)
def test(self, v):
passed = False
if not any(v in t for t in self.types):
raise AssertionError("Neither type in Or holds")
def generate(self):
ng = (e for t in self.types for e in t.generate())
for g in ng:
yield g
[docs]class Maybe(Or):
"""Either the given type or None.
One Type may be passed to Maybe. Maybe checks to see whether it
is either an element of the passed type or else None. This is
useful for optional arguments which take None as the default
parameter.
"""
def __repr__(self):
return "Maybe(" + repr(self.types[0]) + ")"
def __init__(self, typ):
super().__init__(typ, Nothing)
[docs]class Not(Type):
"""Valid if the given type fails.
Takes one type as an argument. Valid values are not of this type.
Note that this should be avoided when possible, as it cannot
generate values. It is most useful within an And clause.
"""
def __init__(self, typ):
super().__init__(typ)
self.type = TypeFactory(typ)
def test(self, v):
assert not (v in self.type), "Not clause does not hold"
def generate(self):
pass
class PositionalArguments(Type):
"""Function optional positional arguments.
This is used internally.
"""
def test(self, v):
super().test(v)
assert isinstance(v, tuple), "Non-dict passed"
def generate(self):
yield ()
class KeywordArguments(Type):
"""Function optional keyword arguments.
This is used internally.
"""
def test(self, v):
super().test(v)
assert isinstance(v, dict), "Non-dict passed"
for e in v.keys():
isinstance(e, str)
def generate(self):
yield {}