mirror of
https://github.com/python/cpython.git
synced 2024-12-30 04:05:10 +08:00
b83861f026
Patch by Erik Welch. bpo-19072 (#8405) allows `classmethod` to wrap other descriptors, but this does not work when the wrapped descriptor mimics classmethod. The current PR fixes this. In Python 3.8 and before, one could create a callable descriptor such that this works as expected (see Lib/test/test_decorators.py for examples): ```python class A: @myclassmethod def f1(cls): return cls @classmethod @myclassmethod def f2(cls): return cls ``` In Python 3.8 and before, `A.f2()` return `A`. Currently in Python 3.9, it returns `type(A)`. This PR make `A.f2()` return `A` again. As of #8405, classmethod calls `obj.__get__(type)` if `obj` has `__get__`. This allows one to chain `@classmethod` and `@property` together. When using classmethod-like descriptors, it's the second argument to `__get__`--the owner or the type--that is important, but this argument is currently missing. Since it is None, the "owner" argument is assumed to be the type of the first argument, which, in this case, is wrong (we want `A`, not `type(A)`). This PR updates classmethod to call `obj.__get__(type, type)` if `obj` has `__get__`. Co-authored-by: Erik Welch <erik.n.welch@gmail.com>
456 lines
14 KiB
Python
456 lines
14 KiB
Python
from test import support
|
|
import unittest
|
|
from types import MethodType
|
|
|
|
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.__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.__name__
|
|
counts[func_name] = 0
|
|
def call(*args, **kwds):
|
|
counts[func_name] += 1
|
|
return func(*args, **kwds)
|
|
call.__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.__name__ = 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 check_wrapper_attrs(self, method_wrapper, format_str):
|
|
def func(x):
|
|
return x
|
|
wrapper = method_wrapper(func)
|
|
|
|
self.assertIs(wrapper.__func__, func)
|
|
self.assertIs(wrapper.__wrapped__, func)
|
|
|
|
for attr in ('__module__', '__qualname__', '__name__',
|
|
'__doc__', '__annotations__'):
|
|
self.assertIs(getattr(wrapper, attr),
|
|
getattr(func, attr))
|
|
|
|
self.assertEqual(repr(wrapper), format_str.format(func))
|
|
return wrapper
|
|
|
|
def test_staticmethod(self):
|
|
wrapper = self.check_wrapper_attrs(staticmethod, '<staticmethod({!r})>')
|
|
|
|
# bpo-43682: Static methods are callable since Python 3.10
|
|
self.assertEqual(wrapper(1), 1)
|
|
|
|
def test_classmethod(self):
|
|
wrapper = self.check_wrapper_attrs(classmethod, '<classmethod({!r})>')
|
|
|
|
self.assertRaises(TypeError, wrapper, 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.__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 SyntaxErrors:
|
|
for stmt in ("x,", "x, y", "x = y", "pass", "import sys"):
|
|
compile(stmt, "test", "exec") # Sanity check.
|
|
with self.assertRaises(SyntaxError):
|
|
compile(f"@{stmt}\ndef f(): pass", "test", "exec")
|
|
|
|
# Test TypeErrors that used to be SyntaxErrors:
|
|
for expr in ("1.+2j", "[1, 2][-1]", "(1, 2)", "True", "...", "None"):
|
|
compile(expr, "test", "eval") # Sanity check.
|
|
with self.assertRaises(TypeError):
|
|
exec(f"@{expr}\ndef f(): pass")
|
|
|
|
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_expressions(self):
|
|
for expr in (
|
|
"(x,)", "(x, y)", "x := y", "(x := y)", "x @y", "(x @ y)", "x[0]",
|
|
"w[x].y.z", "w + x - (y + z)", "x(y)()(z)", "[w, x, y][z]", "x.y",
|
|
):
|
|
compile(f"@{expr}\ndef f(): pass", "test", "exec")
|
|
|
|
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_wrapped_descriptor_inside_classmethod(self):
|
|
class BoundWrapper:
|
|
def __init__(self, wrapped):
|
|
self.__wrapped__ = wrapped
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
return self.__wrapped__(*args, **kwargs)
|
|
|
|
class Wrapper:
|
|
def __init__(self, wrapped):
|
|
self.__wrapped__ = wrapped
|
|
|
|
def __get__(self, instance, owner):
|
|
bound_function = self.__wrapped__.__get__(instance, owner)
|
|
return BoundWrapper(bound_function)
|
|
|
|
def decorator(wrapped):
|
|
return Wrapper(wrapped)
|
|
|
|
class Class:
|
|
@decorator
|
|
@classmethod
|
|
def inner(cls):
|
|
# This should already work.
|
|
return 'spam'
|
|
|
|
@classmethod
|
|
@decorator
|
|
def outer(cls):
|
|
# Raised TypeError with a message saying that the 'Wrapper'
|
|
# object is not callable.
|
|
return 'eggs'
|
|
|
|
self.assertEqual(Class.inner(), 'spam')
|
|
self.assertEqual(Class.outer(), 'eggs')
|
|
self.assertEqual(Class().inner(), 'spam')
|
|
self.assertEqual(Class().outer(), 'eggs')
|
|
|
|
def test_wrapped_classmethod_inside_classmethod(self):
|
|
class MyClassMethod1:
|
|
def __init__(self, func):
|
|
self.func = func
|
|
|
|
def __call__(self, cls):
|
|
if hasattr(self.func, '__get__'):
|
|
return self.func.__get__(cls, cls)()
|
|
return self.func(cls)
|
|
|
|
def __get__(self, instance, owner=None):
|
|
if owner is None:
|
|
owner = type(instance)
|
|
return MethodType(self, owner)
|
|
|
|
class MyClassMethod2:
|
|
def __init__(self, func):
|
|
if isinstance(func, classmethod):
|
|
func = func.__func__
|
|
self.func = func
|
|
|
|
def __call__(self, cls):
|
|
return self.func(cls)
|
|
|
|
def __get__(self, instance, owner=None):
|
|
if owner is None:
|
|
owner = type(instance)
|
|
return MethodType(self, owner)
|
|
|
|
for myclassmethod in [MyClassMethod1, MyClassMethod2]:
|
|
class A:
|
|
@myclassmethod
|
|
def f1(cls):
|
|
return cls
|
|
|
|
@classmethod
|
|
@myclassmethod
|
|
def f2(cls):
|
|
return cls
|
|
|
|
@myclassmethod
|
|
@classmethod
|
|
def f3(cls):
|
|
return cls
|
|
|
|
@classmethod
|
|
@classmethod
|
|
def f4(cls):
|
|
return cls
|
|
|
|
@myclassmethod
|
|
@MyClassMethod1
|
|
def f5(cls):
|
|
return cls
|
|
|
|
@myclassmethod
|
|
@MyClassMethod2
|
|
def f6(cls):
|
|
return cls
|
|
|
|
self.assertIs(A.f1(), A)
|
|
self.assertIs(A.f2(), A)
|
|
self.assertIs(A.f3(), A)
|
|
self.assertIs(A.f4(), A)
|
|
self.assertIs(A.f5(), A)
|
|
self.assertIs(A.f6(), A)
|
|
a = A()
|
|
self.assertIs(a.f1(), A)
|
|
self.assertIs(a.f2(), A)
|
|
self.assertIs(a.f3(), A)
|
|
self.assertIs(a.f4(), A)
|
|
self.assertIs(a.f5(), A)
|
|
self.assertIs(a.f6(), A)
|
|
|
|
def f(cls):
|
|
return cls
|
|
|
|
self.assertIs(myclassmethod(f).__get__(a)(), A)
|
|
self.assertIs(myclassmethod(f).__get__(a, A)(), A)
|
|
self.assertIs(myclassmethod(f).__get__(A, A)(), A)
|
|
self.assertIs(myclassmethod(f).__get__(A)(), type(A))
|
|
self.assertIs(classmethod(f).__get__(a)(), A)
|
|
self.assertIs(classmethod(f).__get__(a, A)(), A)
|
|
self.assertIs(classmethod(f).__get__(A, A)(), A)
|
|
self.assertIs(classmethod(f).__get__(A)(), type(A))
|
|
|
|
class TestClassDecorators(unittest.TestCase):
|
|
|
|
def test_simple(self):
|
|
def plain(x):
|
|
x.extra = 'Hello'
|
|
return x
|
|
@plain
|
|
class C(object): pass
|
|
self.assertEqual(C.extra, 'Hello')
|
|
|
|
def test_double(self):
|
|
def ten(x):
|
|
x.extra = 10
|
|
return x
|
|
def add_five(x):
|
|
x.extra += 5
|
|
return x
|
|
|
|
@add_five
|
|
@ten
|
|
class C(object): pass
|
|
self.assertEqual(C.extra, 15)
|
|
|
|
def test_order(self):
|
|
def applied_first(x):
|
|
x.extra = 'first'
|
|
return x
|
|
def applied_second(x):
|
|
x.extra = 'second'
|
|
return x
|
|
@applied_second
|
|
@applied_first
|
|
class C(object): pass
|
|
self.assertEqual(C.extra, 'second')
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|