import logging, datetime, functools, unittest, re, io

def pre(condition,errmsg='error'):
	"""
	author:	Martin Robinson
	email:	saloparenator@gmail.com
	project:Defensive
	about:
	apply condition predicate
	if ok call the function
	otherwise it will log and raise exception
	@decorator
	"""
	def decorator(func):
		@functools.wraps(func)
		def closure(*args, **kwargs):
			if condition(*args, **kwargs):
				return func(*args, **kwargs)
			e = Exception('precondition '+errmsg)
			raise e
		if condition:
			return closure
		return func
	return decorator

def post(condition,errmsg='error'):
	"""
	author:	Martin Robinson
	email:	saloparenator@gmail.com
	project:Defensive
	about:
	apply function and test result with condition predicate
	if fail it will log and raise exception
	@decorator
	"""
	def decorator(func):
		@functools.wraps(func)
		def closure(*args, **kwargs):
			ret = func(*args, **kwargs)
			if condition(ret):
				return ret
			e = Exception('postcondition '+errmsg)
			raise e
		if condition:
			return closure
		return func
	return decorator

def arg(*types):
	"""
	author:	Martin Robinson
	email:	saloparenator@gmail.com
	project:Defensive
	about:
	reenforce type safety on a function argument
	@decorator
	"""
	def decorator(func):
		@functools.wraps(func)
		def closure(*args, **kwargs):
			if len(types)!=len(args):
				raise  Exception('type error')
			for expect,actual in zip(tuple(types),tuple(map(type,args))):
				if hasattr(expect, '__iter__'):
					if not actual in expect:
						raise  Exception('type error')
				elif actual != expect:
						raise  Exception('type error')
			return func(*args, **kwargs)
		return closure
	return decorator

def ret(*types):
	"""
	author:	Martin Robinson
	email:	saloparenator@gmail.com
	project:Defensive
	about:
	reenforce type safety on a function return value
	@decorator
	"""
	def decorator(func):
		@functools.wraps(func)
		def closure(*args, **kwargs):
			retvals = func(*args, **kwargs)
			if isinstance(retvals, tuple):
				if tuple(types) != tuple(map(type,retvals)):
					raise  Exception('type error')
			elif types != (type(retvals),):
				raise  Exception('type error')
			return retvals
		return closure
	return decorator

class Logging():
	def __init__(self,name='defensive'):
		"""
		author:	Martin Robinson
		email:	saloparenator@gmail.com
		project:Defensive
		about:
		this marvelous class is intended for defensive programming
		absolutely no performance benefits
		only use as annotation on function and method
		"""
		self.logger = logging.getLogger(name)

	def debug(self,func):
		"""
		add debug logging behavior to the function
		will log the exception if one is raised
		[date time functionName (args kwargs)]
		[date time functionName => return]
		@decorator
		"""
		@functools.wraps(func)
		def closure(*args, **kwargs):
			self.logger.debug('%s %s (%s %s)',\
						 datetime.datetime.now(),\
						 func.__name__,\
						 str(args),\
						 str(kwargs))
			try:
				ret = func(*args, **kwargs)
				self.logger.debug('%s %s => %s',\
							 datetime.datetime.now(),\
							 func.__name__,\
							 str(ret))
				return ret
			except Exception as e:
				self.logger.exception(e)
				raise e
		return closure

	def deprecated(self,msg=""):
		"""
		simply log deprecation warning
		@decorator
		"""
		def decorator(func):
			@functools.wraps(func)
			def closure(*args, **kwargs):
				self.logger.warn('%s call %s (deprecated) %s',\
							 datetime.datetime.now(),\
							 func.__name__,\
							 str(msg))
				try:
					ret = func(*args, **kwargs)
					return ret
				except Exception as e:
					self.logger.exception(e)
					raise e
			return closure
		return decorator
		
class Testing(object):
	def __init__(self,clazz=None,*args,**kwargs):
		"""
		author:	Martin Robinson
		email:	saloparenator@gmail.com
		project:Testing
		about:
		testing decorator
		only use as annotation on function
		will work with object method 
		if you provide object instant to runTest method
		"""
		self.logger = logging.getLogger('defensive')
		self._testLoader = loader = unittest.TestLoader()
		self._testSuite = []
		self._logCapture = io.BytesIO()
		self._logHandler = logging.StreamHandler(self._logCapture)
		self._obj = None
		if clazz:
			self._obj = clazz(*args,**kwargs)

	def _addTest(self,testClass):
		testLoaded = self._testLoader.loadTestsFromTestCase(testClass)
		self._testSuite.append(testLoaded)
	
	def runTest(self,obj=None):
		"""
		run test suite with all test annotation used from this object
		unit testing
		"""
		if obj:
			self._obj = obj
		self.logger.setLevel(logging.DEBUG)
		self.logger.addHandler(self._logHandler)
		suite = unittest.TestSuite(self._testSuite)
		runner = unittest.TextTestRunner()
		return runner.run(suite)

	def eq(self,result,*args,**kwargs):
		"""
		from first argument as expected result
		execute the function with remaining argument
		then compare result with expected one
		unit testing
		@decorator
		"""
		def getObj():
			return self._obj
		def decorator(func):
			class InnerEqTest(unittest.TestCase):
				@functools.wraps(func)
				def test(self):
					if not getObj():
						ret = func(*args,**kwargs)
					else:
						ret = func(getObj(),*args,**kwargs)
					self.assertTrue(result == ret,'equality test error with '+func.__name__+'('+str(args)+') yield '+ str(result)+' but '+str(ret)+' was expected')
			self._addTest(InnerEqTest)
			return func
		return decorator

	def true(self,*args,**kwargs):
		"""
		with True as expected result
		execute the function with remaining argument
		then compare result with True
		unit testing
		@decorator
		"""
		def getObj():
			return self._obj
		def decorator(func):
			class InnerEqTest(unittest.TestCase):
				@functools.wraps(func)
				def test(self):
					if not getObj():
						ret = func(*args,**kwargs)
					else:
						ret = func(getObj(),*args,**kwargs)
					self.assertTrue(ret,'truth test error with '+func.__name__+'('+str(args)+')')
			self._addTest(InnerEqTest)
			return func
		return decorator
		
	def exception(self,exceptionClass,*args,**kwargs):
		"""
		first argument as expected exception class
		execute function with given param
		make sure it raise exception
		@decorator
		"""
		def getObj():
			return self._obj
		def decorator(func):
			class InnerExceptionTest(unittest.TestCase):
				def test(self):
					with self.assertRaises(exceptionClass):
						if not getObj():
							func(*args,**kwargs)
						else:
							func(getObj(),*args,**kwargs)
						msg ='exception test error with '+func.__name__+'('+str(args)+') '+str(exceptionClass)+' was expected'
			self._addTest(InnerExceptionTest)
			return func
		return decorator
	
	def log(self,expression,*args,**kwargs):
		"""
		verify that the function append string that match given regular expression
		while running with given param
		@decorator
		"""
		obj = self._obj
		logCapture = self._logCapture
		def getObj():
			return self._obj
		def decorator(func):
			class InnerLogTest(unittest.TestCase):
				def test(self):
					try:
						if not getObj():
							func(*args,**kwargs)
						else:
							func(getObj(),*args,**kwargs)
					except:
						pass
					log = logCapture.getvalue()
					self.assertTrue(not re.findall(expression,log) is None,'log test error with '+func.__name__+'('+str(args)+') log result : '+log+' | dont match '+expression)
			self._addTest(InnerLogTest)
			return func
		return decorator


if __name__ == '__main__':
	d = Logging()
	t = Testing()

	@t.exception(Exception)
	@post(lambda a:a!=4)
	def test4():
		return 4
	
	@t.log('.+test3arg.+',1,2,3)
	@d.debug
	def test3Arg(a,b,c):
		pass
	
	@t.log('.+testRet.+',5)
	@t.exception(Exception,-1)
	@t.exception(Exception,11)
	@d.debug	
	@pre(lambda a:a>0)
	@post(lambda a:a<10)
	def testRet(a):
		return a
	
	@t.eq(2,1)
	@t.eq(3,2)
	def testInc(a):
		return a+1;
		
	@t.eq(2,1,1)
	@t.eq(3,2,1)
	def testAdd(a,b):
		return a+b;
		
	@t.eq(0)
	def testZero():
		return 0;

	@t.exception(KeyboardInterrupt)
	def testRaise():
		raise KeyboardInterrupt()

	print t.runTest()
	
	################################################################
	
	t = Testing()
	class DefensiveObj(Testing):
		def __init__(self):
			pass

		@t.log('.+ret.+',5)
		@t.exception(Exception,-1)
		@t.exception(Exception,10)
		@t.eq(2,2)
		@d.debug
		@pre(lambda s,a:a>0)
		@post(lambda a:a<10)
		def ret(self,a):
			return a
	
	dd = DefensiveObj()
	
	print t.runTest(dd)

	t = Testing()
	class DefensiveObj2(Testing):
		def __init__(self,a):
			self.a = a

		@t.log('.+ret.+',5)
		@t.exception(Exception,-1)
		@t.exception(Exception,10)
		@t.eq(5,2)
		@d.debug
		@pre(lambda s,a:a>0)
		@post(lambda a:a<10)
		def ret(self,a):
			return a+self.a
	
	dd = DefensiveObj2(3)
	
	print t.runTest(dd)
	
	###############################################################
	t=Testing()
	@t.eq(2,3,1)
	@t.exception(Exception,1,'1')
	@t.exception(Exception,'1',1)
	@arg(int,int)
	def sub(a,b):
		return a-b

	@t.eq(1,1)
	@t.exception(Exception,'1')
	@ret(int)
	def rete(a):
		return a
	
	@t.eq((1,2,3),1,2,3)
	@t.exception(Exception,'1',2,3)
	@t.exception(Exception,1,2,'3')
	@t.exception(Exception,1,'2',3)
	@ret(int,int,int)
	def t2t(a,b,c):
		return a,b,c
		
	@t.eq(3,3,1)
	@t.eq(1,None,1)
	@t.exception(Exception,None,None)
	@arg((int,str,type(None)),(int,str))
	def coalesce(a,b):
		return a if a else b
	
	@t.eq({'a':'a'},'a','a')
	@t.eq({'a':1},'a',1)
	@t.eq({'a':None},'a',None)
	@t.eq({'a':[]},'a',[])
	@t.exception(Exception,None,None)
	@arg(str,(list,tuple,str,int,type(None)))
	def any(name,val):
		return {name:val}
	
	print t.runTest()
