diff --git a/tabbed/packages/typecheck/__init__.py b/tabbed/packages/typecheck/__init__.py new file mode 100644 index 0000000..66ce74e --- /dev/null +++ b/tabbed/packages/typecheck/__init__.py @@ -0,0 +1,1542 @@ +__all__ = ['accepts', 'returns', 'yields', 'TypeCheckError', 'Length', 'Empty' + ,'TypeSignatureError', 'And', 'Any', 'Class', 'Exact', 'HasAttr' + ,'IsAllOf', 'IsCallable', 'IsIterable', 'IsNoneOf', 'IsOneOf' + ,'IsOnlyOneOf', 'Not', 'Or', 'Self', 'Xor', 'YieldSeq' + ,'register_type', 'is_registered_type', 'unregister_type' + ,'Function'] + +import inspect +import types + +from types import GeneratorType, FunctionType, MethodType, ClassType, TypeType + +# Controls whether typechecking is on (True) or off (False) +enable_checking = True + +# Pretty little wrapper function around __typecheck__ +def check_type(type, func, val): + type.__typecheck__(func, val) + +### Internal exception classes (these MUST NOT get out to the user) +### typecheck_{args,return,yield} should catch these and convert them to +### appropriate Type{Check,Signature}Error instances + +# We can't inherit from object because raise doesn't like new-style classes +# We can't use super() because we can't inherit from object +class _TC_Exception(Exception): + def error_message(self): + raise NotImplementedError("Incomplete _TC_Exception subclass (%s)" % str(self.__class__)) + + def format_bad_object(self, bad_object): + return ("for %s, " % str(bad_object), self) + +class _TC_LengthError(_TC_Exception): + def __init__(self, wrong, right=None): + _TC_Exception.__init__(self) + + self.wrong = wrong + self.right = right + + def error_message(self): + m = None + if self.right is not None: + m = ", expected %d" % self.right + return "length was %d%s" % (self.wrong, m or "") + +class _TC_TypeError(_TC_Exception): + def __init__(self, wrong, right): + _TC_Exception.__init__(self) + + self.wrong = calculate_type(wrong) + self.right = right + + def error_message(self): + return "expected %s, got %s" % (self.right, self.wrong) + +class _TC_NestedError(_TC_Exception): + def __init__(self, inner_exception): + self.inner = inner_exception + + def error_message(self): + try: + return ", " + self.inner.error_message() + except: + print "'%s'" % self.inner.message + raw_input() + raise + +class _TC_IndexError(_TC_NestedError): + def __init__(self, index, inner_exception): + _TC_NestedError.__init__(self, inner_exception) + + self.index = index + + def error_message(self): + return ("at index %d" % self.index) + _TC_NestedError.error_message(self) + +# _TC_DictError exists as a wrapper around dict-related exceptions. +# It provides a single place to sort the bad dictionary's keys in the error +# message. +class _TC_DictError(_TC_NestedError): + def format_bad_object(self, bad_object): + message = "for {%s}, " % ', '.join(["%s: %s" % (repr(k), repr(bad_object[k])) for k in sorted(bad_object.keys())]) + + if not isinstance(self.inner, _TC_LengthError): + return (message, self) + return (message, self.inner) + + def error_message(self): + raise NotImplementedError("Incomplete _TC_DictError subclass: " + str(self.__class__)) + +class _TC_KeyError(_TC_DictError): + def __init__(self, key, inner_exception): + _TC_NestedError.__init__(self, inner_exception) + + self.key = key + + def error_message(self): + return ("for key %s" % repr(self.key)) + _TC_NestedError.error_message(self) + +class _TC_KeyValError(_TC_KeyError): + def __init__(self, key, val, inner_exception): + _TC_KeyError.__init__(self, key, inner_exception) + + self.val = val + + def error_message(self): + return ("at key %s, value %s" % (repr(self.key), repr(self.val))) + _TC_NestedError.error_message(self) + +class _TC_GeneratorError(_TC_NestedError): + def __init__(self, yield_no, inner_exception): + _TC_NestedError.__init__(self, inner_exception) + + self.yield_no = yield_no + + def error_message(self): + raise RuntimeError("_TC_GeneratorError.message should never be called") + + def format_bad_object(self, bad_object): + bad_obj, start_message = self.inner.format_bad_object(bad_object) + message = "At yield #%d: %s" % (self.yield_no, bad_obj) + return (message, start_message) + +### These next three exceptions exist to give HasAttr better error messages +class _TC_AttrException(_TC_Exception): + def __init__(self, attr): + _TC_Exception.__init__(self, attr) + + self.attr = attr + +class _TC_AttrError(_TC_AttrException, _TC_NestedError): + def __init__(self, attr, inner_exception): + _TC_AttrException.__init__(self, attr) + _TC_NestedError.__init__(self, inner_exception) + + def error_message(self): + return ("as for attribute %s" % self.attr) + _TC_NestedError.error_message(self) + +class _TC_MissingAttrError(_TC_AttrException): + def error_message(self): + return "missing attribute %s" % self.attr + +# This is like _TC_LengthError for YieldSeq +class _TC_YieldCountError(_TC_Exception): + def __init__(self, expected): + _TC_Exception.__init__(self, expected) + + self.expected = expected + + def format_bad_object(self, bad_object): + return ("", self) + + def error_message(self): + plural = "s" + if self.expected == 1: + plural = "" + + return "only expected the generator to yield %d time%s" % (self.expected, plural) + +# This exists to provide more detailed error messages about why a given +# Xor() assertion failed +class _TC_XorError(_TC_NestedError): + def __init__(self, matched_conds, inner_exception): + assert matched_conds in (0, 2) + assert isinstance(inner_exception, _TC_TypeError) + + _TC_Exception.__init__(self, matched_conds, inner_exception) + _TC_NestedError.__init__(self, inner_exception) + self.matched_conds = matched_conds + + def error_message(self): + if self.matched_conds == 0: + m = "neither assertion" + else: + m = "both assertions" + + return _TC_NestedError.error_message(self) + " (matched %s)" % m + +class _TC_FunctionError(_TC_Exception): + def __init__(self, checking_func, obj): + self.checking_func = checking_func + self.rejected_obj = obj + + def error_message(self): + return " was rejected by %s" % self.checking_func + + def format_bad_object(self, bad_object): + return (str(bad_object), self) + +class _TC_ExactError(_TC_Exception): + def __init__(self, wrong, right): + self.wrong = wrong + self.right = right + + def error_message(self): + return "expected %s, got %s" % (self.right, self.wrong) + +### The following exist to provide detailed TypeSignatureErrors +class _TS_Exception(Exception): + def error_message(self): + raise NotImplementedError("Incomplete _TS_Exception subclass (%s)" % str(self.__class__)) + +# This is used when there was an error related to an auto-unpacked tuple +# in the function's signature +class _TS_TupleError(_TS_Exception): + def __init__(self, parameters, types): + parameters = _rec_tuple(parameters) + types = _rec_tuple(types) + _TS_Exception.__init__(self, parameters, types) + + self.parameters = parameters + self.types = types + + def error_message(self): + return "the signature type %s does not match %s" % (str(self.types), str(self.parameters)) + +class _TS_ExtraKeywordError(_TS_Exception): + def __init__(self, keyword): + _TS_Exception.__init__(self, keyword) + + self.keyword = keyword + + def error_message(self): + return "the keyword '%s' in the signature is not in the function" % self.keyword + +class _TS_ExtraPositionalError(_TS_Exception): + def __init__(self, type): + _TS_Exception.__init__(self, type) + + self.type = type + + def error_message(self): + return "an extra positional type has been supplied" + +class _TS_MissingTypeError(_TS_Exception): + def __init__(self, parameter): + _TS_Exception.__init__(self, parameter) + + self.parameter = parameter + + def error_message(self): + return "parameter '%s' lacks a type" % self.parameter + +# If the user has given a keyword parameter a type both positionally and +# with a keyword argument, this will be raised +class _TS_TwiceTypedError(_TS_Exception): + def __init__(self, parameter, kw_type, pos_type): + _TS_Exception.__init__(self, parameter, kw_type, pos_type) + + self.parameter = parameter + self.kw_type = kw_type + self.pos_type = pos_type + + def error_message(self): + return "parameter '%s' is provided two types (%s and %s)" % (self.parameter, str(self.kw_type), str(self.pos_type)) + +### The following functions are the way new type handlers are registered +### The Type function will iterate over all registered type handlers; +### the first handler to return a non-None value is considered the winner +######################################################################### + +_hooks = ("__typesig__", "__startchecking__", "__stopchecking__", "__switchchecking__") + +_registered_types = set() +_registered_hooks = dict([(_h, set()) for _h in _hooks]) + +def _manage_registration(add_remove, reg_type): + if not isinstance(reg_type, (types.ClassType, types.TypeType)): + raise ValueError("registered types must be classes or types") + + valid = False + for hook in _hooks: + if hasattr(reg_type, hook): + getattr(_registered_hooks[hook], add_remove)(reg_type) + valid = True + + if valid: + getattr(_registered_types, add_remove)(reg_type) + else: + raise ValueError("registered types must have at least one of the following methods: " + ", ".join(_hooks)) + +def register_type(reg_type): + _manage_registration('add', reg_type) + +def unregister_type(reg_type): + _manage_registration('remove', reg_type) + +def is_registered_type(reg_type): + return reg_type in _registered_types + +### Factory function; this is what should be used to dispatch +### type-checker class requests + +def Type(obj): + # Note that registered types cannot count on being run in a certain order; + # their __typesig__ methods must be sufficiently flexible to account for + # this + for reg_type in _registered_hooks['__typesig__']: + v = reg_type.__typesig__(obj) + if v is not None: + return v + + raise AssertionError("Object is of type '%s'; not a type" % str(type(obj))) + +def __checking(start_stop, *args): + attr = '__%schecking__' % start_stop + + for reg_type in _registered_hooks[attr]: + getattr(reg_type, attr)(*args) + +def start_checking(function): + __checking('start', function) + +def stop_checking(function): + __checking('stop', function) + +def switch_checking(from_func, to_func): + for reg_type in _registered_types: + if hasattr(reg_type, '__switchchecking__'): + getattr(reg_type, '__switchchecking__')(from_func, to_func) + else: + if hasattr(reg_type, '__stopchecking__'): + getattr(reg_type, '__stopchecking__')(from_func) + if hasattr(reg_type, '__startchecking__'): + getattr(reg_type, '__startchecking__')(to_func) + +### Deduce the type of a data structure +### +### XXX: Find a way to allow registered utility classes +### to hook into this +def calculate_type(obj): + if isinstance(obj, types.InstanceType): + return obj.__class__ + elif isinstance(obj, dict): + if len(obj) == 0: + return {} + + key_types = set() + val_types = set() + + for (k,v) in obj.items(): + key_types.add( calculate_type(k) ) + val_types.add( calculate_type(v) ) + + if len(key_types) == 1: + key_types = key_types.pop() + else: + key_types = Or(*key_types) + + if len(val_types) == 1: + val_types = val_types.pop() + else: + val_types = Or(*val_types) + + return {key_types: val_types} + elif isinstance(obj, tuple): + return tuple([calculate_type(t) for t in obj]) + elif isinstance(obj, list): + length = len(obj) + if length == 0: + return [] + obj = [calculate_type(o) for o in obj] + + partitions = [1] + partitions.extend([i for i in range(2, int(length/2)+1) if length%i==0]) + partitions.append(length) + + def evaluate(items_per): + parts = length / items_per + + for i in range(0, parts): + for j in range(0, items_per): + if obj[items_per * i + j] != obj[j]: + raise StopIteration + return obj[0:items_per] + + for items_per in partitions: + try: + return evaluate(items_per) + except StopIteration: + continue + else: + return type(obj) + +### The following classes are the work-horses of the typechecker + +# The base class for all the other utility classes +class CheckType(object): + def __repr__(self): + return type(self).name + '(' + ', '.join(sorted(repr(t) for t in self._types)) + ')' + + __str__ = __repr__ + + def __eq__(self, other): + return not self != other + + def __ne__(self, other): + return not self == other + + def __hash__(self): + raise NotImplementedError("Incomplete CheckType subclass: %s" % self.__class__) + + def __typecheck__(self, func, obj): + raise NotImplementedError("Incomplete CheckType subclass: %s" % self.__class__) + + @classmethod + def __typesig__(cls, obj): + if isinstance(obj, CheckType): + return obj + +class Single(CheckType): + name = "Single" + + def __init__(self, type): + if not isinstance(type, (types.ClassType, types.TypeType)): + raise TypeError("Cannot type-check a %s" % type(type)) + else: + self.type = type + + self._types = [self.type] + + def __typecheck__(self, func, to_check): + if not isinstance(to_check, self.type): + raise _TC_TypeError(to_check, self.type) + + def __eq__(self, other): + if other.__class__ is not self.__class__: + return False + return self.type == other.type + + def __hash__(self): + return hash(str(hash(self.__class__)) + str(hash(self.type))) + + # XXX Is this really a good idea? + # Removing this only breaks 3 tests; that seems suspiciously low + def __repr__(self): + return repr(self.type) + + @classmethod + def __typesig__(cls, obj): + if isinstance(obj, (types.ClassType, types.TypeType)): + return Single(obj) + +### Provide a way to enforce the empty-ness of iterators +class Empty(Single): + name = "Empty" + + def __init__(self, type): + if not hasattr(type, '__len__'): + raise TypeError("Can only assert emptyness for types with __len__ methods") + + Single.__init__(self, type) + + def __typecheck__(self, func, to_check): + Single.__typecheck__(self, func, to_check) + + if len(to_check) > 0: + err = _TC_LengthError(len(to_check), 0) + if isinstance(to_check, dict): + raise _TC_DictError(err) + raise err + +class Dict(CheckType): + name = "Dict" + + def __init__(self, key, val): + self.__check_key = Type(key) + self.__check_val = Type(val) + + self.type = {key: val} + self._types = [key, val] + + def __typecheck__(self, func, to_check): + if not isinstance(to_check, types.DictType): + raise _TC_TypeError(to_check, self.type) + + for (k, v) in to_check.items(): + # Check the key + try: + check_type(self.__check_key, func, k) + except _TC_Exception, inner: + raise _TC_KeyError(k, inner) + + # Check the value + try: + check_type(self.__check_val, func, v) + except _TC_Exception, inner: + raise _TC_KeyValError(k, v, inner) + + def __eq__(self, other): + if other.__class__ is not self.__class__: + return False + return self.type == other.type + + def __hash__(self): + cls = self.__class__ + key = self.__check_key + val = self.__check_val + + def strhash(obj): + return str(hash(obj)) + + return hash(''.join(map(strhash, [cls, key, val]))) + + @classmethod + def __typesig__(cls, obj): + if isinstance(obj, dict): + if len(obj) == 0: + return Empty(dict) + return Dict(obj.keys()[0], obj.values()[0]) + +### Provide typechecking for the built-in list() type +class List(CheckType): + name = "List" + + def __init__(self, *type): + self._types = [Type(t) for t in type] + self.type = [t.type for t in self._types] + + def __typecheck__(self, func, to_check): + if not isinstance(to_check, list): + raise _TC_TypeError(to_check, self.type) + if len(to_check) % len(self._types): + raise _TC_LengthError(len(to_check)) + + # lists can be patterned, meaning that [int, float] + # requires that the to-be-checked list contain an alternating + # sequence of integers and floats. The pattern must be completed + # (e.g, [5, 5.0, 6, 6.0] but not [5, 5.0, 6]) for the list to + # typecheck successfully. + # + # A list with a single type, [int], is a sub-case of patterned + # lists + # + # XXX: Investigate speed increases by special-casing single-typed + # lists + pat_len = len(self._types) + type_tuples = [(i, val, self._types[i % pat_len]) for (i, val) + in enumerate(to_check)] + for (i, val, type) in type_tuples: + try: + check_type(type, func, val) + except _TC_Exception, e: + raise _TC_IndexError(i, e) + + def __eq__(self, other): + if other.__class__ is not self.__class__: + return False + + if len(self._types) != len(other._types): + return False + + for (s, o) in zip(self._types, other._types): + if s != o: + return False + return True + + def __hash__(self): + def strhash(obj): + return str(hash(obj)) + + return hash(''.join(map(strhash, [self.__class__] + self._types))) + + @classmethod + def __typesig__(cls, obj): + if isinstance(obj, list): + if len(obj) == 0: + return Empty(list) + return List(*obj) + +### Provide typechecking for the built-in tuple() class +class Tuple(List): + name = "Tuple" + + def __init__(self, *type): + List.__init__(self, *type) + + self.type = tuple(self.type) + + def __typecheck__(self, func, to_check): + # Note that tuples of varying length (e.g., (int, int) and (int, int, int)) + # are separate types, not merely differences in length like lists + if not isinstance(to_check, types.TupleType) or len(to_check) != len(self._types): + raise _TC_TypeError(to_check, self.type) + + for (i, (val, type)) in enumerate(zip(to_check, self._types)): + try: + check_type(type, func, val) + except _TC_Exception, inner: + raise _TC_IndexError(i, inner) + + @classmethod + def __typesig__(cls, obj): + if isinstance(obj, tuple): + return Tuple(*obj) + +class TypeVariables(CheckType): + # This is a stack of {typevariable -> type} mappings + # It is intentional that it is class-wide; it maintains + # the mappings of the outer functions if we descend into + # nested typechecked functions + __mapping_stack = [] + + # This is the {typevariable -> type} mapping for the function + # currently being checked + __active_mapping = None + + # This dict maps generators to their mappings + __gen_mappings = {} + + def __init__(self, name): + self.type = name + + def __str__(self): + return "TypeVariable(%s)" % self.type + + __repr__ = __str__ + + def __hash__(self): + return hash(''.join([str(o) for o in self.__class__ + , hash(type(self.type)) + , hash(self.type)])) + + def __eq__(self, other): + if self.__class__ is not other.__class__: + return False + return type(self.type) is type(other.type) and self.type == other.type + + def __typecheck__(self, func, to_check): + name = self.type + if isinstance(func, GeneratorType): + active = self.__class__.__gen_mappings[func] + else: + active = self.__class__.__active_mapping + + # We have to do this because u'a' == 'a' + lookup = (name, type(name)) + if lookup in active: + check_type(active[lookup], func, to_check) + else: + # This is the first time we've encountered this + # typevariable for this function call. + # + # In this case, we automatically approve the object + active[lookup] = Type(calculate_type(to_check)) + + @classmethod + def __typesig__(cls, obj): + if isinstance(obj, basestring): + return cls(obj) + + @classmethod + def __startchecking__(cls, func): + if isinstance(func, GeneratorType): + cls.__gen_mappings.setdefault(func, {}) + elif isinstance(func, FunctionType): + cls.__mapping_stack.append(cls.__active_mapping) + cls.__active_mapping = {} + else: + raise TypeError(func) + + @classmethod + def __switchchecking__(cls, from_func, to_func): + if isinstance(from_func, FunctionType): + if isinstance(to_func, GeneratorType): + cls.__gen_mappings[to_func] = cls.__active_mapping + cls.__stopchecking__(from_func) + elif isinstance(to_func, FunctionType): + cls.__stopchecking__(from_func) + cls.__startchecking__(to_func) + else: + raise TypeError(to_func) + else: + raise TypeError(from_func) + + @classmethod + def __stopchecking__(cls, func): + if isinstance(func, GeneratorType): + del cls.__gen_mappings[func] + elif isinstance(func, FunctionType): + cls.__active_mapping = cls.__mapping_stack.pop() + else: + raise TypeError(func) + +class Function(CheckType): + def __init__(self, func): + self._func = func + self.type = self + + @classmethod + def __typesig__(cls, obj): + if isinstance(obj, (FunctionType, MethodType)): + return cls(obj) + + # Snag callable class instances (that aren't types or classes) + if type(obj) not in (types.ClassType, type) and callable(obj): + return cls(obj) + + def __typecheck__(self, func, to_check): + if False == self._func(to_check): + raise _TC_FunctionError(self._func, to_check) + + def __str__(self): + return "Function(%s)" % self._func + + def __repr__(self): + return str(self) + + def __eq__(self, other): + if self.__class__ is not other.__class__: + return False + return self._func is other._func + + def __hash__(self): + return hash(str(self.__class__) + str(hash(self._func))) + +# Register some of the above types so that Type() knows about them +for c in (CheckType, List, Tuple, Dict, Single, TypeVariables, Function): + register_type(c) + +### The following are utility classes intended to make writing complex +### signatures easier. +###################################################################### + +### Instances of Any() automatically approve of the object they're supposed +### to be checking (ie, they don't actually check it; use this with caution) +class Any(CheckType): + name = "Any" + + def __init__(self): + self.type = object + + def __typecheck__(self, func, to_check): + pass + + def __str__(self): + return "Any()" + + __repr__ = __str__ + + # All instances of this class are equal + def __eq__(self, other): + return other.__class__ is self.__class__ + + def __hash__(self): + return hash(self.__class__) + +### Base class for Or() and And() +class _Boolean(CheckType): + def __init__(self, first_type, second_type, *types): + self._types = set() + + for t in (first_type, second_type)+types: + if type(t) is type(self): + self._types.update(t._types) + else: + self._types.add(Type(t)) + + if len(self._types) < 2: + raise TypeError("there must be at least 2 distinct parameters to __init__()") + + self.type = self + + def __eq__(self, other): + if other.__class__ is not self.__class__: + return False + + return self._types == other._types + + def __hash__(self): + return hash(str(hash(self.__class__)) + str(hash(frozenset(self._types)))) + +class Or(_Boolean): + name = "Or" + + def __typecheck__(self, func, to_check): + for type in self._types: + try: + check_type(type, func, to_check) + return + except _TC_Exception: + pass + + raise _TC_TypeError(to_check, self) + +class And(_Boolean): + name = "And" + + def __typecheck__(self, func, to_check): + for type in self._types: + try: + check_type(type, func, to_check) + except _TC_Exception, e: + raise _TC_TypeError(to_check, self) + +class Not(Or): + name = "Not" + + # We override _Boolean's __init__ so that we can accept a single + # condition + def __init__(self, first_type, *types): + self._types = set([Type(t) for t in (first_type,)+types]) + + self.type = self + + def __typecheck__(self, func, to_check): + # Or does our work for us, but we invert its result + try: + Or.__typecheck__(self, func, to_check) + except _TC_Exception: + return + raise _TC_TypeError(to_check, self) + +class Xor(_Boolean): + name = "Xor" + + def __typecheck__(self, func, to_check): + already_met_1_cond = False + + for typ in self._types: + try: + check_type(typ, func, to_check) + except _TC_Exception: + pass + else: + if already_met_1_cond: + raise _TC_XorError(2, _TC_TypeError(to_check, self)) + already_met_1_cond = True + + if not already_met_1_cond: + raise _TC_XorError(0, _TC_TypeError(to_check, self)) + +class IsCallable(CheckType): + def __init__(self): + self.type = self + + def __str__(self): + return "IsCallable()" + + __repr__ = __str__ + + # They're all the same + # XXX Change IsCallable to a singleton class + def __hash__(self): + return id(self.__class__) + + def __eq__(self, other): + return self.__class__ is other.__class__ + + def __typecheck__(self, func, to_check): + if not callable(to_check): + raise _TC_TypeError(to_check, 'a callable') + +class HasAttr(CheckType): + def __init__(self, set_1, set_2=None): + attr_sets = {list: [], dict: {}} + + for (arg_1, arg_2) in ((set_1, set_2), (set_2, set_1)): + for t in (list, dict): + if isinstance(arg_1, t): + attr_sets[t] = arg_1 + if isinstance(arg_2, t): + raise TypeError("can only have one list and/or one dict") + + self._attr_types = dict.fromkeys(attr_sets[list], Any()) + + for (attr, typ) in attr_sets[dict].items(): + self._attr_types[attr] = Type(typ) + + def __typecheck__(self, func, to_check): + for (attr, typ) in self._attr_types.items(): + if not hasattr(to_check, attr): + raise _TC_MissingAttrError(attr) + + try: + check_type(typ, func, getattr(to_check, attr)) + except _TC_Exception, e: + raise _TC_AttrError(attr, e) + + def __eq__(self, other): + if self.__class__ is not other.__class__: + return False + return self._attr_types == other._attr_types + + def __hash__(self): + return hash(str(hash(self.__class__)) + str(hash(str(self._attr_types)))) + + def __str__(self): + any_type = [] + spec_type = {} + + any = Any() + + for (attr, typ) in self._attr_types.items(): + if typ == any: + any_type.append(attr) + else: + spec_type[attr] = typ + + msg = [t for t in (any_type, spec_type) if len(t)] + + return "HasAttr(" + ', '.join(map(str, msg)) + ")" + + __repr__ = __str__ + +class IsIterable(CheckType): + def __init__(self): + self.type = self + + def __eq__(self, other): + return self.__class__ is other.__class__ + + # They're all the same + # XXX Change IsIterable to a singleton class + def __hash__(self): + return id(self.__class__) + + def __str__(self): + return "IsIterable()" + + __repr__ = __str__ + + def __typecheck__(self, func, to_check): + if not (hasattr(to_check, '__iter__') and callable(to_check.__iter__)): + raise _TC_TypeError(to_check, "an iterable") + +class YieldSeq(CheckType): + _index_map = {} + + def __init__(self, type_1, type_2, *types): + self.type = self + + self._type = [type_1, type_2] + list(types) + self._types = [Type(t) for t in self._type] + + def __hash__(self): + return id(self) + + def __str__(self): + return "YieldSeq(" + ", ".join(map(str, self._type)) + ")" + + __repr__ = __str__ + + def __eq__(self, other): + if self.__class__ is not other.__class__: + return False + return self._types == other._types + + def __hash__(self): + return hash(str(self.__class__) + str([hash(t) for t in self._types])) + + # We have to use __{start,stop}checking__ so that the indexes get + # reset every time we run through the typechecking sequence + @classmethod + def __startchecking__(cls, gen): + if isinstance(gen, GeneratorType): + cls._index_map[gen] = {} + + @classmethod + def __stopchecking__(cls, gen): + if gen in cls._index_map: + del cls._index_map[gen] + + def __typecheck__(self, gen, to_check): + index_map = self.__class__._index_map + + # There might be multiple YieldSeq's per signature + if self not in index_map[gen]: + index_map[gen][self] = -1 + index = index_map[gen] + + if index[self] >= len(self._types)-1: + raise _TC_YieldCountError(len(self._types)) + + index[self] += 1 + check_type(self._types[index[self]], gen, to_check) + +register_type(YieldSeq) + +class Exact(CheckType): + def __init__(self, obj): + self.type = self + self._obj = obj + + def __hash__(self): + try: + obj_hash = str(hash(self._obj)) + except TypeError: + obj_hash = str(type(self._obj)) + str(self._obj) + + return hash(str(self.__class__) + obj_hash) + + def __eq__(self, other): + if self.__class__ is not other.__class__: + return False + return self._obj == other._obj + + def __typecheck__(self, func, to_check): + if self._obj != to_check: + raise _TC_ExactError(to_check, self._obj) + +class Length(CheckType): + def __init__(self, length): + self.type = self + self._length = int(length) + + def __hash__(self): + return hash(str(self.__class__) + str(self._length)) + + def __eq__(self, other): + if self.__class__ is not other.__class__: + return False + return self._length == other._length + + def __typecheck__(self, func, to_check): + try: + length = len(to_check) + except TypeError: + raise _TC_TypeError(to_check, "something with a __len__ method") + + if length != self._length: + raise _TC_LengthError(length, self._length) + +import sys +class Class(CheckType): + def __init__(self, class_name): + self.type = self + self.class_name = class_name + self.class_obj = None + self._frame = sys._getframe(1) + + def __hash__(self): + return hash(str(self.__class__) + self.class_name) + + def __str__(self): + return "Class('%s')" % self.class_name + + __repr__ = __str__ + + def __eq__(self, other): + if self.__class__ is not other.__class__: + return False + return self.class_name == other.class_name + + def __typecheck__(self, func, to_check): + if self.class_obj is None: + class_name = self.class_name + frame = self._frame + + for f_dict in (frame.f_locals, frame.f_globals): + if class_name in frame.f_locals: + if self is not frame.f_locals[class_name]: + self.class_obj = frame.f_locals[class_name] + self._frame = None + break + else: + raise NameError("name '%s' is not defined" % class_name) + + if not isinstance(to_check, self.class_obj): + raise _TC_TypeError(to_check, self.class_obj) + +class Typeclass(CheckType): + bad_members = dict.fromkeys(['__class__', '__new__', '__init__'], True) + + def __init__(self, *types): + if len(types) == 0: + raise TypeError("Must supply at least one type to __init__()") + + self.type = self + + self._cache = set() + self._interface = set() + self._instances = set() + for t in types: + self.add_instance(t) + + self._calculate_interface() + + def recalculate_interface(self): + self._cache = self._instances.copy() + self._calculate_interface() + + def instances(self): + return list(self._instances) + + def interface(self): + return list(self._interface) + + def has_instance(self, instance): + return instance in self._instances + + def add_instance(self, instance): + if isinstance(instance, self.__class__): + for inst in instance.instances(): + self._instances.add(inst) + self._cache.add(inst) + elif isinstance(instance, (ClassType, TypeType)): + self._instances.add(instance) + self._cache.add(instance) + else: + raise TypeError("All instances must be classes or types") + + def intersect(self, other): + if isinstance(other, self.__class__): + new_instances = other.instances() + else: + new_instances = other + + self._instances.update(new_instances) + self._cache.update(new_instances) + self._calculate_interface() + + def _calculate_interface(self): + bad_members = self.bad_members + + for instance in self._instances: + inst_attrs = [] + + for attr, obj in instance.__dict__.items(): + if callable(obj) and attr not in bad_members: + inst_attrs.append(attr) + + if len(self._interface) == 0: + self._interface = set(inst_attrs) + else: + self._interface.intersection_update(inst_attrs) + + def __typecheck__(self, func, to_check): + if to_check.__class__ in self._cache: + return + + for method in self._interface: + if not hasattr(to_check, method): + raise _TC_MissingAttrError(method) + + attr = getattr(to_check, method) + if not callable(attr): + raise _TC_AttrError(method, _TC_TypeError(attr, IsCallable())) + + self._cache.add(to_check.__class__) + + def __eq__(self, other): + if self.__class__ is not other.__class__: + return False + return self._instances == other._instances + + def __hash__(self): + return hash(str(self.__class__) + str(hash(frozenset(self._instances)))) + + def __repr__(self): + return object.__repr__(self) + + def __str__(self): + return 'Typeclass(' + ', '.join(map(str, self._instances)) + ')' + +# The current implementation of Self relies on the TypeVariables machinery +_Self = TypeVariables("this is the class of the invocant") +def Self(): + return _Self + +### Aliases +########### + +IsOneOf = Or +IsAllOf = And +IsNoneOf = Not +IsOnlyOneOf = Xor + +### This is the public side of the module +######################################### + +# This is for backwards compatibility with v0.1.6 and earlier +class TypeCheckException(Exception): + pass + +class TypeCheckError(TypeCheckException): + def __init__(self, prefix, bad_object, exception): + TypeCheckException.__init__(self, prefix, bad_object, exception) + + self.prefix = prefix + self.internal = exception + self.bad_object = bad_object + + (bad_obj_str, start_message) = exception.format_bad_object(bad_object) + self.__message = prefix + bad_obj_str + start_message.error_message() + + def __str__(self): + return self.__message + +class TypeSignatureError(Exception): + def __init__(self, internal_exc): + Exception.__init__(self, internal_exc) + + self.internal = internal_exc + self.__message = internal_exc.error_message() + + def __str__(self): + return self.__message + +### Begin helper classes/functions for typecheck_args +##################################################### +def _rec_tuple(obj): + if isinstance(obj, list): + return tuple(_rec_tuple(o) for o in obj) + return obj + +def _rec_tuple_str(obj): + if not isinstance(obj, (list, tuple)): + return obj + + if len(obj) == 1: + return '(%s,)' % obj + + return '(' + ', '.join(_rec_tuple_str(o) for o in obj) + ')' + +def _gen_arg_to_param(func, (posargs, varargs, varkw, defaults)): + sig_args = list() + dic_args = list() + + for obj in posargs: + if isinstance(obj, list): + rts = _rec_tuple_str(obj) + + sig_args.append(rts) + dic_args.append((_rec_tuple(obj), rts)) + else: + sig_args.append(str(obj)) + dic_args.append(('"%s"' % obj, obj)) + + func_code = '' + if varargs: + dic_args.append(('"%s"' % varargs, varargs)) + sig_args.append('*' + varargs) + func_code = '\n\t%s = list(%s)' % (varargs, varargs) + if varkw: + dic_args.append(('"%s"' % varkw, varkw)) + sig_args.append('**' + varkw) + + func_name = func.func_name + '_' + while func_name in dic_args: + func_name += '_' + + func_def = 'def %s(' % func.func_name + func_return = func_code \ + + '\n\treturn {' \ + + ', '.join('%s: %s' % kv for kv in dic_args) \ + + '}' + + locals = {} + exec func_def + ','.join(sig_args) + '):' + func_return in locals + func = locals[func.func_name] + func.func_defaults = defaults + return func + +def _validate_tuple(ref, obj): + if not isinstance(ref, (list, tuple)): + return + if not isinstance(obj, (list, tuple)): + raise _TS_TupleError(ref, obj) + + if len(ref) != len(obj): + raise _TS_TupleError(ref, obj) + + try: + for r, o in zip(ref, obj): + _validate_tuple(r, o) + except _TS_TupleError: + raise _TS_TupleError(ref, obj) + +def _param_to_type((params, varg_name, kwarg_name), vargs, kwargs): + vargs = list(vargs) + kwargs = dict(kwargs) + + # Make parameter names to values + param_value = dict() + + # There are excess positional arguments, but no *args parameter + if len(params) < len(vargs) and varg_name is None: + raise _TS_ExtraPositionalError(vargs[len(params)]) + # There are not enough position args and no kwargs to draw from + if len(params) > len(vargs) and len(kwargs) == 0: + raise _TS_MissingTypeError(params[len(vargs)]) + + # No reason to do this if there aren't any vargs + if len(vargs): + for p, a in zip(params, vargs): + # Make sure all auto-unpacked tuples match up + _validate_tuple(p, a) + param_value[_rec_tuple(p)] = a + + # No reason to do all this work if there aren't any kwargs + if len(kwargs) > 0: + # All params that still need values + params = set([k for k in params if k not in param_value]) + if kwarg_name and kwarg_name not in param_value: + params.add(kwarg_name) + if varg_name and varg_name not in param_value: + params.add(varg_name) + + # Lift this out of the loop + no_double_star = kwarg_name is None + + # All parameter slots have been filled, but there are still keyword + # args remaining with no **kwargs parameter present + if len(params) == 0 and no_double_star: + raise _TS_ExtraKeywordError(kwargs.keys()[0]) + + # Match up remaining keyword args with open parameter slots + for p, a in kwargs.items(): + if p in param_value: + raise _TS_TwiceTypedError(p, a, param_value[p]) + if p not in params and no_double_star: + raise _TS_ExtraKeywordError(p) + + # Make sure all auto-unpacked tuples match up + _validate_tuple(p, a) + + # Bookkeeping + params.remove(p) + param_value[p] = a + + # Any elements left in params indicate that the parameter is missing + # a value + if len(params): + raise _TS_MissingTypeError(params.pop()) + + return param_value + +def _make_fake_function(func): + def fake_function(*vargs, **kwargs): + # We call start_checking here, but __check_result + # has to call stop_checking on its own. The reason + # for this is so that typecheck_yield can call + # stop_checking on the function and then start_checking + # on the generator + start_checking(func) + + # If either one of these operations fails, we need to call + # stop_checking() + try: + fake_function.__check_args(vargs, kwargs) + result = func(*vargs, **kwargs) + except: + stop_checking(func) + raise + + return fake_function.__check_result(func, result) + + # These are the default implementations of __check_args + # and __check_results + def _pass_args(vargs, kwargs): + pass + def _pass_result(func, result): + stop_checking(func) + return result + + fake_function.__check_args = _pass_args + fake_function.__check_result = _pass_result + fake_function.__wrapped_func = func + + # Mock-up the fake function to look as much like the + # real function as possible + fake_function.__module__ = func.__module__ + fake_function.__name__ = func.__name__ + fake_function.__doc__ = func.__doc__ + + return fake_function + +################################################### +### End helper classes/functions for typecheck_args + +def typecheck_args(*v_sig, **kw_sig): + # typecheck_args is run to obtain the real decorator + def decorator(func): + if hasattr(func, '__wrapped_func'): + if hasattr(func, 'type_args'): + raise RuntimeError('Cannot use the same typecheck_* function more than once on the same function') + wrapped_func = func.__wrapped_func + else: + wrapped_func = func + + param_list, varg_name, kwarg_name, defaults = inspect.getargspec(wrapped_func) + args_to_params = _gen_arg_to_param(wrapped_func, (param_list, varg_name, kwarg_name, defaults)) + + try: + param_types = _param_to_type((param_list, varg_name, kwarg_name), v_sig, kw_sig) + except _TS_Exception, e: + raise TypeSignatureError(e) + + ### We need to fix-up the types of the *vargs and **kwargs parameters + ##################################################################### + if varg_name: + if not isinstance(param_types[varg_name], list): + param_types[varg_name] = [param_types[varg_name]] + + if kwarg_name: + if not isinstance(param_types[kwarg_name], dict): + param_types[kwarg_name] = {str: param_types[kwarg_name]} + + ##################################################################### + ### /Fix-up + + # Convert the signatures to types now, rather than rebuild them in every function call + check_param_types = dict() + for k, v in param_types.items(): + check_param_types[k] = Type(v) + + def __check_args(__vargs, __kwargs): + # Type-checking can be turned on and off by toggling the + # value of the global enable_checking variable + if enable_checking: + arg_dict = args_to_params(*__vargs, **__kwargs) + + # Type-check the keyword arguments + try: + for name, val in arg_dict.items(): + check_type(check_param_types[name], wrapped_func, val) + except _TC_Exception, e: + str_name = _rec_tuple_str(name) + raise TypeCheckError("Argument %s: " % str_name, val, e) + + if hasattr(func, '__check_result'): + # This is one of our wrapper functions, probably created by + # typecheck_yield or typecheck_return + fake_function = func + else: + # We need to build a wrapper + fake_function = _make_fake_function(func) + + # Specify how argument checking should be done + fake_function.__check_args = __check_args + + ### Add the publically-accessible signature information + fake_function.type_args = param_types + + return fake_function + return decorator + +# Refactor this out of typecheck_{return,yield} +def _decorator(signature, conflict_field, twice_field, check_result_func): + def decorator(func): + if hasattr(func, '__check_result'): + # This is one of our wrapper functions, probably created by + # typecheck_args + if hasattr(func, conflict_field): + raise RuntimeError("Cannot use typecheck_return and typecheck_yield on the same function") + elif hasattr(func, twice_field): + raise RuntimeError('Cannot use the same typecheck_* function more than once on the same function') + + fake_function = func + else: + fake_function = _make_fake_function(func) + + setattr(fake_function, twice_field, signature) + fake_function.__check_result = check_result_func + return fake_function + return decorator + +def typecheck_return(*signature): + if len(signature) == 1: + signature = signature[0] + sig_types = Type(signature) + + def __check_return(func, return_vals): + if enable_checking: + try: + check_type(sig_types, func, return_vals) + except _TC_Exception, e: + stop_checking(func) + raise TypeCheckError("Return value: ", return_vals, e) + + stop_checking(func) + return return_vals + return _decorator(signature, 'type_yield', 'type_return', __check_return) + +class Fake_generator(object): + def __init__(self, real_gen, signature): + # The generator should have the same yield signature + # as the function that produced it; however, we don't + # copy the args signature because the generator + # doesn't take arguments + self.type_yield = signature + + self.__yield_no = 0 + self.__real_gen = real_gen + self.__sig_types = Type(signature) + self.__needs_stopping = True + + def next(self): + gen = self.__real_gen + + self.__yield_no += 1 + + try: + return_vals = gen.next() + except StopIteration: + if self.__needs_stopping: + stop_checking(gen) + self.__needs_stopping = False + raise + + if enable_checking: + try: + check_type(self.__sig_types, gen, return_vals) + except _TC_Exception, e: + # Insert this error into the chain so we can know + # which yield the error occurred at + middle_exc = _TC_GeneratorError(self.__yield_no, e) + raise TypeCheckError("", return_vals, middle_exc) + + # Everything checks out. Return the results + return return_vals + + def __del__(self): + if self.__needs_stopping: + stop_checking(self.__real_gen) + +def typecheck_yield(*signature): + if len(signature) == 1: + signature = signature[0] + + def __check_yield(func, gen): + # If the return value isn't a generator, we blow up + if not isinstance(gen, types.GeneratorType): + stop_checking(func) + raise TypeError("typecheck_yield only works for generators") + + # Inform all listening classes that they might want to preserve any information + # from the function to the generator (*hint* TypeVariables *hint*) + # + # stop_checking() will not be invoked on the generator until it raises + # StopIteration or its refcount drops to 0 + switch_checking(func, gen) + + # Otherwise, we build ourselves a fake generator + return Fake_generator(gen, signature) + return _decorator(signature, 'type_return', 'type_yield', __check_yield) + +# Aliases +typecheck = typecheck_args +accepts = typecheck_args +returns = typecheck_return +yields = typecheck_yield diff --git a/tabbed/packages/typecheck/doctest_support.py b/tabbed/packages/typecheck/doctest_support.py new file mode 100644 index 0000000..0933dda --- /dev/null +++ b/tabbed/packages/typecheck/doctest_support.py @@ -0,0 +1,36 @@ +""" +This module allows doctest to find typechecked functions. + +Currently, doctest verifies functions to make sure that their +globals() dict is the __dict__ of their module. In the case of +decorated functions, the globals() dict *is* not the right one. + +To enable support for doctest do: + + import typecheck.doctest_support + +This import must occur before any calls to doctest methods. +""" + +def __DocTestFinder_from_module(self, module, object): + """ + Return true if the given object is defined in the given + module. + """ + import inspect + + if module is None: + return True + elif inspect.isfunction(object) or inspect.isclass(object): + return module.__name__ == object.__module__ + elif inspect.getmodule(object) is not None: + return module is inspect.getmodule(object) + elif hasattr(object, '__module__'): + return module.__name__ == object.__module__ + elif isinstance(object, property): + return True # [XX] no way not be sure. + else: + raise ValueError("object must be a class or function") + +import doctest as __doctest +__doctest.DocTestFinder._from_module = __DocTestFinder_from_module \ No newline at end of file diff --git a/tabbed/packages/typecheck/mixins.py b/tabbed/packages/typecheck/mixins.py new file mode 100644 index 0000000..df346c5 --- /dev/null +++ b/tabbed/packages/typecheck/mixins.py @@ -0,0 +1,84 @@ +from typecheck import _TC_NestedError, _TC_TypeError, check_type, Or +from typecheck import register_type, _TC_Exception + +class _TC_IterationError(_TC_NestedError): + def __init__(self, iteration, value, inner_exception): + _TC_NestedError.__init__(self, inner_exception) + + self.iteration = iteration + self.value = value + + def error_message(self): + return ("at iteration %d (value: %s)" % (self.iteration, repr(self.value))) + _TC_NestedError.error_message(self) + +### This is the shadow class behind UnorderedIteratorMixin. +### Again, it tries to pretend it doesn't exist by mimicing +### the class of as much as possible. +### +### This mixin provides typechecking for iterator classes +### where you don't care about the order of the types (ie, +### you simply Or() the types together, as opposed to patterned +### lists, which would be ordered mixins) +class _UnorderedIteratorMixin(object): + def __init__(self, class_name, obj): + vals = [o for o in obj] + + self.type = self + self._type = Or(*vals) + self.__cls = obj.__class__ + self.__vals = vals + # This is necessary because it's a huge pain in the ass + # to get the "raw" name of the class once it's created + self.__cls_name = class_name + + def __typecheck__(self, func, to_check): + if not isinstance(to_check, self.__cls): + raise _TC_TypeError(to_check, self) + + for i, item in enumerate(to_check): + try: + check_type(self._type, func, item) + except _TC_Exception, e: + raise _TC_IterationError(i, item, e) + + @classmethod + def __typesig__(cls, obj): + if isinstance(obj, cls): + return obj + + def __str__(self): + return "%s(%s)" % (self.__cls_name, str(self._type)) + + __repr__ = __str__ + +### This is included in a class's parent-class section like so: +### class MyClass(UnorderedIteratorMixin("MyClass")): +### blah blah blah +### +### This serves as a class factory, whose produced classes +### attempt to mask the fact they exist. Their purpose +### is to redirect __typesig__ calls to appropriate +### instances of _UnorderedIteratorMixin +def UnorderedIteratorMixin(class_name): + class UIM(object): + @classmethod + def __typesig__(cls, obj): + if isinstance(obj, cls): + return _UnorderedIteratorMixin(class_name, obj) + + def __repr__(self): + return "%s%s" % (class_name, str(tuple(e for e in self))) + + # We register each produced class anew + # If someone needs to unregister these classes, they should + # save a copy of it before including it in the class-definition: + # + # my_UIM = UnorderedIteratorMixin("FooClass") + # class FooClass(my_UIM): + # ... + # + # Alternatively, you could just look in FooClass.__bases__ later; whatever + register_type(UIM) + return UIM + +register_type(_UnorderedIteratorMixin) diff --git a/tabbed/packages/typecheck/sets.py b/tabbed/packages/typecheck/sets.py new file mode 100644 index 0000000..299738c --- /dev/null +++ b/tabbed/packages/typecheck/sets.py @@ -0,0 +1,62 @@ +from typecheck import CheckType, _TC_TypeError, check_type, Type +from typecheck import register_type, Or, _TC_Exception, _TC_KeyError +from typecheck import _TC_LengthError + +### Provide typechecking for the built-in set() class +### +### XXX: Investigate rewriting this in terms of +### UnorderedIteratorMixin or Or() +class Set(CheckType): + def __init__(self, set_list): + self.type = set(set_list) + self._types = [Type(t) for t in self.type] + + # self._type is used to build _TC_TypeError + if len(self._types) > 1: + self._type = Or(*self.type) + elif len(self._types) == 1: + # XXX Is there an easier way to get this? + t = self.type.pop() + self._type = t + self.type.add(t) + + def __str__(self): + return "Set(" + str([e for e in self.type]) + ")" + + __repr__ = __str__ + + def __typecheck__(self, func, to_check): + if not isinstance(to_check, set): + raise _TC_TypeError(to_check, self.type) + + if len(self._types) == 0 and len(to_check) > 0: + raise _TC_LengthError(len(to_check), 0) + + for obj in to_check: + error = False + for type in self._types: + try: + check_type(type, func, obj) + except _TC_Exception: + error = True + continue + else: + error = False + break + if error: + raise _TC_KeyError(obj, _TC_TypeError(obj, self._type)) + + def __eq__(self, other): + if self.__class__ is not other.__class__: + return False + return self.type == other.type + + def __hash__(self): + return hash(str(hash(self.__class__)) + str(hash(frozenset(self.type)))) + + @classmethod + def __typesig__(self, obj): + if isinstance(obj, set): + return Set(obj) + +register_type(Set) diff --git a/tabbed/packages/typecheck/typeclasses.py b/tabbed/packages/typecheck/typeclasses.py new file mode 100644 index 0000000..1be57a0 --- /dev/null +++ b/tabbed/packages/typecheck/typeclasses.py @@ -0,0 +1,35 @@ +from typecheck import Typeclass + +### Number +#################################################### + +_numbers = [int, float, complex, long, bool] +try: + from decimal import Decimal + _numbers.append(Decimal) + del Decimal +except ImportError: + pass + +Number = Typeclass(*_numbers) +del _numbers + +### String -- subinstance of ImSequence +#################################################### + +String = Typeclass(str, unicode) + +### ImSequence -- immutable sequences +#################################################### + +ImSequence = Typeclass(tuple, xrange, String) + +### MSequence -- mutable sequences +#################################################### + +MSequence = Typeclass(list) + +### Mapping +#################################################### + +Mapping = Typeclass(dict)