import types, operator import traceback, sys ############# Error Handling ############ class InterfaceError(TypeError): def __init__(self, name, val, type): self.name, self.val, self.type = name, val, type self.err = repr(self) TypeError.__init__(self, self.err) def __repr__(self): if self.name=="_RETURNS": name = "Return code" else: name = "Parameter '%s'" % self.name return "%s expected %s.\nInstead it got '%s' (%s)" % \ (name, self.type.dump(), \ type(self.val).__name__, repr(self.val)) ############# Basic types ############## # Rather than defining interface abstractions for these types, we will # use the type objects directly. In the future we may swap in my # abstract definitions that would allow (e.g.) integers to pass for # floats or that might convert between types automatically. Complex = type(1j) Int = type(1) Float = type(1.0) Long = type(1L) #INumber: pass # # Python numbers do not actually have that much in common right now. # Floats can be passed to some functions expecting integers but not # others. Integers can be passed to some functions expecting floats # but not others. Until this is cleaned up, it is safest not to # pretend that there is a proper "number" behavior. ########### Interface base class ############## class Interface: """The shared behavior of interface objects""" def __init__(self, name, docstring, checkfunc, bases=(), *args, **kwargs): self.__name__ = name self.__doc__= docstring if bases==(): bases = [Object] self._bases = bases self._checkfunc = checkfunc self.args = args for arg in args: assert arg!=None self.kwargs = kwargs # this allows interfaces to look like object names but be called # like functions when necessary def __call__(self, *args, **kwargs): "For parameterized types -- make a new object with the params" return Interface(self.__name__, self.__doc__, self._checkfunc, self._bases, *args, **kwargs) def __check__(self, obj): for base in self._bases: if not base.__check__(obj): return 0 if self._checkfunc is not None: return self._checkfunc(obj, *self.args, **self.kwargs) return 1 def dump(self): if self.args: args = [arg.dump() for arg in self.args] argstr = "(%s)" + ",".join(args) else: args = "" return "%s%s" % (self.__name__, args) def __repr__(self): return "" % self.dump() ############# Machinery ############### #everything's an object! Object = Interface("Object", "Any Python object", None, []) InterfaceInterface = Interface("Interface", "Interface interface", lambda obj: hasattr(obj, "__check__")) def _unionCheck(obj, *bases): for base in bases: if checkType(obj, base): return 1 return 0 def Union(*args): comma = ", " names = [] for base in args: if base==None: names.append("None") else: names.append(base.__name__) names = comma.join(names) return Interface(None, "Union of %s" % names, lambda obj,args=args: _unionCheck(obj, *args)) def _tupleCheck(obj, *types): try: if len(types)!=len(obj): return 0 except IndexError: return 0 for aval, aniface in zip(obj, types): if not checkType(aval, aniface): return 0 return 1 Tuple = Interface(None, "Tuple", _tupleCheck) ########### Strings ######################## String=Interface("String", "Unicode or 8-bit string", lambda obj: type(obj) in (types.StringType, types.UnicodeType)) ########### Ancient Protocols ################# Undefined = "_____________undefined______________" def _isseq(obj, valuetype=Undefined): if type(obj)==types.InstanceType: if not hasattr( obj, "__getitem__"): return 0 elif not operator.isSequenceType(obj): return 0 if valuetype is not Undefined: try: # check first and last -- that's an approximation! if not checkType(obj[1], valuetype): return 0 if not checkType(obj[-1], valuetype): return 0 except IndexError: pass # empty list is considered good enough return 1 # passed all tests! Sequence = Interface("Sequence", "Some kind of sequence", _isseq) def _ismapping(obj, KeyType = None, ValueType = None): if type(obj)==types.InstanceType: return (hasattr(obj, "__getitem__") and hasattr(obj, "items")) else: return operator.isMappingType(obj) Mapping = Interface("Mapping", "Mapping from keys to values", _ismapping) Callable=Interface("ICallable", "Object that can be called like function", callable) # built-in callable is a little smarter than # types.isSequenceType, etc. StrictBoolean = Interface("StrictBoolean", "0 or 1 value", lambda obj: obj in (0, 1)) ReadStream= Interface("ReadStream", "object that can be read from like a file", lambda obj: hasattr(obj, "read")) WriteStream= Interface("ReadStream", "object that can be read from like a file", lambda obj: hasattr(obj, "read")) def _findInterface(obj, iface): """Helper function that figures out if an interface is in an objects __interfaces__ list. Note that objects with a single interface may not bother to wrap it in a list. That's why this function is not trivial.""" ifaces = getattr(obj, "__interfaces__", None) if type(ifaces) not in (types.ListType, types.TupleType): ifaces = [ifaces] #normalize the single interface shortcut if iface in ifaces: return 1 # now look for all of the interface's base interfaces in the # object. for base in iface._bases: if _findInterface(obj, base): return 1 # give up return 0 def checkType(val, iface): "Checks that an object adheres to a type" if iface == None == iface: return 1 # if the iface is a tuple or list, we do unpacking on the value if type(iface) in (types.TupleType, types.ListType): return _tupleCheck(val, iface) # if the iface is just a class or type, we just use isinstance elif type(iface) in (types.ClassType, types.TypeType): return isinstance(val, iface) # if the object CLAIMS to support an interface, we believe it elif _findInterface(val, iface): return 1 # if the object LOOKS like it supports an interface, good 'nuff else: return iface.__check__(val) ####### Strictness (exception throwing or warning) ############ _g_strict = None def setstrict(strict): global _g_strict _g_strict = strict ######## Magic runtime functions ############ def __paramcheck__(localsdict=None, typesdict=None): """Magic function to check all params in a localsdict against type declarations in a typesdict""" # XXX one day we should make the params required and dump this # code. for descr, typ in typesdict.items(): name=descr.split(":")[0] if name in ("_RETURNS", "self", "__types__"): continue obj = localsdict[name] if not checkType(obj, typ): raise InterfaceError(name, obj, typ) def __rccheck__(val, localsdict, typesdict): "Like paramcheck for locals" for descr, typ in typesdict.items(): name=descr.split(":")[0] if name=="_RETURNS" and not checkType(val, typ): raise InterfaceError(name, val, typ) return val def __typewarn__(exception): # XXX use Python 2.1 warning features when available if _g_strict is None: import os setstrict( os.getenv("PYTHON_STRICT_TYPECHECK")) if _g_strict: raise exception else: # of course this should use the Python 2.1 warning # framework eventually! print "Type error:", exception def __declare__(*args, **args): "dummy function for declaration recognition purposes" ############ test code ################## def _test1(module, integer, func, stream): __types__ = {"module: A module object": types.ModuleType, "integer: An integer object": Int, "func: A callable object": Callable, "stream: A readable stream": ReadStream, "_RETURNS: Return value": Int} __paramcheck__(locals(), __types__) return __rccheck__(5, locals(), __types__) def _test2(tuple, string, interface, interface2): __types__ = {"tuple: A sequence": Sequence, "string: A string": String, "interface: An interface obj": Interface, "interface2: An interface obj": Interface} __paramcheck__(locals(), __types__) def _test3(object, object2, object3): __types__ = {"object: Any old object": Object, "object2: Any old object": Object, "object3: Any old object": Object, "_RETURNS: A pair of objects": (Object, Object) } __paramcheck__(locals(), __types__) return __rccheck__((object, object2), locals(), __types__) if __name__=="__main__": # use doctest eventually! import os _test1(os, 5, _test2, os.popen("blah")) _test2((), "abc", String, String) _test3((), "abc", None) try: _test1(1, 2, 3, 4) assert 0, "Should not have got to this point!" except InterfaceError: pass # expect this exception