Viewing file: test_decorators.py (8.77 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
import unittest from test import test_support
def funcattrs(**kwds): def decorate(func): func.__dict__.update(kwds) return func return decorate
class MiscDecorators (object): @staticmethod def author(name): def decorate(func): func.__dict__['author'] = name return func return decorate
# -----------------------------------------------
class DbcheckError (Exception): def __init__(self, exprstr, func, args, kwds): # A real version of this would set attributes here Exception.__init__(self, "dbcheck %r failed (func=%s args=%s kwds=%s)" % (exprstr, func, args, kwds))
def dbcheck(exprstr, globals=None, locals=None): "Decorator to implement debugging assertions" def decorate(func): expr = compile(exprstr, "dbcheck-%s" % func.func_name, "eval") def check(*args, **kwds): if not eval(expr, globals, locals): raise DbcheckError(exprstr, func, args, kwds) return func(*args, **kwds) return check return decorate
# -----------------------------------------------
def countcalls(counts): "Decorator to count calls to a function" def decorate(func): func_name = func.func_name counts[func_name] = 0 def call(*args, **kwds): counts[func_name] += 1 return func(*args, **kwds) call.func_name = func_name return call return decorate
# -----------------------------------------------
def memoize(func): saved = {} def call(*args): try: return saved[args] except KeyError: res = func(*args) saved[args] = res return res except TypeError: # Unhashable argument return func(*args) call.func_name = func.func_name return call
# -----------------------------------------------
class TestDecorators(unittest.TestCase):
def test_single(self): class C(object): @staticmethod def foo(): return 42 self.assertEqual(C.foo(), 42) self.assertEqual(C().foo(), 42)
def test_staticmethod_function(self): @staticmethod def notamethod(x): return x self.assertRaises(TypeError, notamethod, 1)
def test_dotted(self): decorators = MiscDecorators() @decorators.author('Cleese') def foo(): return 42 self.assertEqual(foo(), 42) self.assertEqual(foo.author, 'Cleese')
def test_argforms(self): # A few tests of argument passing, as we use restricted form # of expressions for decorators.
def noteargs(*args, **kwds): def decorate(func): setattr(func, 'dbval', (args, kwds)) return func return decorate
args = ( 'Now', 'is', 'the', 'time' ) kwds = dict(one=1, two=2) @noteargs(*args, **kwds) def f1(): return 42 self.assertEqual(f1(), 42) self.assertEqual(f1.dbval, (args, kwds))
@noteargs('terry', 'gilliam', eric='idle', john='cleese') def f2(): return 84 self.assertEqual(f2(), 84) self.assertEqual(f2.dbval, (('terry', 'gilliam'), dict(eric='idle', john='cleese')))
@noteargs(1, 2,) def f3(): pass self.assertEqual(f3.dbval, ((1, 2), {}))
def test_dbcheck(self): @dbcheck('args[1] is not None') def f(a, b): return a + b self.assertEqual(f(1, 2), 3) self.assertRaises(DbcheckError, f, 1, None)
def test_memoize(self): counts = {}
@memoize @countcalls(counts) def double(x): return x * 2 self.assertEqual(double.func_name, 'double')
self.assertEqual(counts, dict(double=0))
# Only the first call with a given argument bumps the call count: # self.assertEqual(double(2), 4) self.assertEqual(counts['double'], 1) self.assertEqual(double(2), 4) self.assertEqual(counts['double'], 1) self.assertEqual(double(3), 6) self.assertEqual(counts['double'], 2)
# Unhashable arguments do not get memoized: # self.assertEqual(double([10]), [10, 10]) self.assertEqual(counts['double'], 3) self.assertEqual(double([10]), [10, 10]) self.assertEqual(counts['double'], 4)
def test_errors(self): # Test syntax restrictions - these are all compile-time errors: # for expr in [ "1+2", "x[3]", "(1, 2)" ]: # Sanity check: is expr is a valid expression by itself? compile(expr, "testexpr", "exec")
codestr = "@%s\ndef f(): pass" % expr self.assertRaises(SyntaxError, compile, codestr, "test", "exec")
# You can't put multiple decorators on a single line: # self.assertRaises(SyntaxError, compile, "@f1 @f2\ndef f(): pass", "test", "exec")
# Test runtime errors
def unimp(func): raise NotImplementedError context = dict(nullval=None, unimp=unimp)
for expr, exc in [ ("undef", NameError), ("nullval", TypeError), ("nullval.attr", AttributeError), ("unimp", NotImplementedError)]: codestr = "@%s\ndef f(): pass\nassert f() is None" % expr code = compile(codestr, "test", "exec") self.assertRaises(exc, eval, code, context)
def test_double(self): class C(object): @funcattrs(abc=1, xyz="haha") @funcattrs(booh=42) def foo(self): return 42 self.assertEqual(C().foo(), 42) self.assertEqual(C.foo.abc, 1) self.assertEqual(C.foo.xyz, "haha") self.assertEqual(C.foo.booh, 42)
def test_order(self): # Test that decorators are applied in the proper order to the function # they are decorating. def callnum(num): """Decorator factory that returns a decorator that replaces the passed-in function with one that returns the value of 'num'""" def deco(func): return lambda: num return deco @callnum(2) @callnum(1) def foo(): return 42 self.assertEqual(foo(), 2, "Application order of decorators is incorrect")
def test_eval_order(self): # Evaluating a decorated function involves four steps for each # decorator-maker (the function that returns a decorator): # # 1: Evaluate the decorator-maker name # 2: Evaluate the decorator-maker arguments (if any) # 3: Call the decorator-maker to make a decorator # 4: Call the decorator # # When there are multiple decorators, these steps should be # performed in the above order for each decorator, but we should # iterate through the decorators in the reverse of the order they # appear in the source.
actions = []
def make_decorator(tag): actions.append('makedec' + tag) def decorate(func): actions.append('calldec' + tag) return func return decorate
class NameLookupTracer (object): def __init__(self, index): self.index = index
def __getattr__(self, fname): if fname == 'make_decorator': opname, res = ('evalname', make_decorator) elif fname == 'arg': opname, res = ('evalargs', str(self.index)) else: assert False, "Unknown attrname %s" % fname actions.append('%s%d' % (opname, self.index)) return res
c1, c2, c3 = map(NameLookupTracer, [ 1, 2, 3 ])
expected_actions = [ 'evalname1', 'evalargs1', 'makedec1', 'evalname2', 'evalargs2', 'makedec2', 'evalname3', 'evalargs3', 'makedec3', 'calldec3', 'calldec2', 'calldec1' ]
actions = [] @c1.make_decorator(c1.arg) @c2.make_decorator(c2.arg) @c3.make_decorator(c3.arg) def foo(): return 42 self.assertEqual(foo(), 42)
self.assertEqual(actions, expected_actions)
# Test the equivalence claim in chapter 7 of the reference manual. # actions = [] def bar(): return 42 bar = c1.make_decorator(c1.arg)(c2.make_decorator(c2.arg)(c3.make_decorator(c3.arg)(bar))) self.assertEqual(bar(), 42) self.assertEqual(actions, expected_actions)
def test_main(): test_support.run_unittest(TestDecorators)
if __name__=="__main__": test_main()
|