Commit 91a434d5 authored by Kirill Smelkov's avatar Kirill Smelkov

golang: Add support for @func(Class) and @func to be used over @property

Since the beginning of pygolang it is possible to define methods
separate from class. For example

    @func(MyClass)
    def my_method(self, ...):
        ...

will define MyClass.my_method(*). This works for regular functions and
staticmethod/classmethod as well. But support for properties was missing
because there was no use case so far.

-> Add support for properties as well as I hit the need for it during my
work on wendelin.core monitoring.

Test class changed to inherit from object since on py2 properties work
only for new-style classes.

(*) see afa46cf5 (Turn pygopath into full pygolang) and 942ee900
    (golang: Deprecate @method(cls) in favour of @func(cls)) for details.

/reviewed-by @levin.zimmermann
/reviewed-on nexedi/pygolang!31
parent 5302558e
Pipeline #38409 passed with stage
in 0 seconds
......@@ -73,10 +73,25 @@ def _meth(cls, fcall):
# wrap f with @_func, so that e.g. defer works automatically.
f = _func(f)
if isinstance(f, (staticmethod, classmethod)):
func_name = f.__func__.__name__
# property is special - it has up to 3 functions inside
# but the only practical case is when it starts with
#
# @func(Class)
# @property
# def ...
#
# which means that .fget should be set and so we use get name as the
# name of the method.
f_ = f
if isinstance(f, property):
f_ = f.fget
if f_ is None:
raise ValueError("func(cls) used on property without getter")
if isinstance(f_, (staticmethod, classmethod)):
func_name = f_.__func__.__name__
else:
func_name = f.__name__
func_name = f_.__name__
setattr(cls, func_name, f)
# if `@func(cls) def name` caller already has `name` set, don't override it
......@@ -110,7 +125,19 @@ class _DelAttrAfterMeth(object):
# _func serves @func.
def _func(f):
# @staticmethod & friends require special care:
# @property is special: there are 3 functions inside and we need to wrap
# them all with repacking back into property.
if isinstance(f, property):
fget = fset = fdel = None
if f.fget is not None:
fget = _func(f.fget)
if f.fset is not None:
fset = _func(f.fset)
if f.fdel is not None:
fdel = _func(f.fdel)
return type(f)(fget, fset, fdel, f.__doc__)
# @staticmethod & friends also require special care:
# unpack f first to original func and then repack back after wrapping.
fclass = None
if isinstance(f, (staticmethod, classmethod)):
......
......@@ -973,7 +973,7 @@ def test_func():
# test how @func(cls) works
# this also implicitly tests just @func, since @func(cls) uses that.
class MyClass:
class MyClass(object):
def __init__(self, v):
self.v = v
......@@ -1013,11 +1013,48 @@ def test_func():
gc.collect() # pypy needs this to trigger _DelAttrAfterMeth GC
assert 'var' not in locals()
vproperty = mproperty_orig = 'vproperty'
@func(MyClass)
@property
def vproperty(self):
"""documentation for vproperty"""
assert isinstance(self, MyClass)
return 'v%s' % self.v
assert vproperty is mproperty_orig
assert vproperty == 'vproperty'
@func(MyClass)
@MyClass.vproperty.setter
def _(self, v):
assert isinstance(self, MyClass)
self.v = v
assert vproperty is mproperty_orig
assert vproperty == 'vproperty'
@func(MyClass)
@MyClass.vproperty.deleter
def _(self):
assert isinstance(self, MyClass)
self.v = 'deleted'
assert vproperty is mproperty_orig
assert vproperty == 'vproperty'
obj = MyClass(4)
assert obj.zzz(4) == 4 + 1
assert obj.mstatic(5) == 5 + 1
assert obj.mcls(7) == 7 + 1
assert obj.var(8) == 8 + 1
assert obj.v == 4 # set by .zzz
assert obj.vproperty == 'v4'
obj.vproperty = 5
assert obj.v == 5
assert obj.vproperty == 'v5'
del obj.vproperty
assert obj.v == 'deleted'
assert obj.vproperty == 'vdeleted'
assert MyClass.vproperty.__doc__ == "documentation for vproperty"""
# this tests that @func (used by @func(cls)) preserves decorated function signature
assert fmtargspec(MyClass.zzz) == '(self, v, x=2, **kkkkwww)'
......@@ -1034,7 +1071,14 @@ def test_func():
assert MyClass.var.__module__ == __name__
assert MyClass.var.__name__ == 'var'
# test that func·func = func (double _func calls will be done internally for
assert MyClass.vproperty.fget.__module__ == __name__
assert MyClass.vproperty.fset.__module__ == __name__
assert MyClass.vproperty.fdel.__module__ == __name__
assert MyClass.vproperty.fget.__name__ == 'vproperty'
assert MyClass.vproperty.fset.__name__ == '_'
assert MyClass.vproperty.fdel.__name__ == '_'
# test that func·func = func (double _func calls are done internally for
# getter when handling @func(@MyClass.vproperty.setter)
def f(): pass
g = func(f)
......@@ -1265,6 +1309,45 @@ def test_deferrecover():
assert v == [7, 2, 1]
# defer in std @property
v = []
class MyClass(object):
@func
@property
def vproperty(self):
"""vproperty doc"""
defer(lambda: v.append(1))
defer(lambda: v.append(3))
defer(lambda: v.append(4))
return 'v'
@func
@vproperty.setter
def vproperty(self, val):
defer(lambda: v.append(1))
defer(lambda: v.append(4))
defer(lambda: v.append(val))
@func
@vproperty.deleter
def vproperty(self):
defer(lambda: v.append(1))
defer(lambda: v.append(5))
defer(lambda: v.append('del'))
obj = MyClass()
assert MyClass.vproperty.__doc__ == "vproperty doc"
assert obj.vproperty == 'v'
assert v == [4, 3, 1]
v = []
obj.vproperty = 'q'
assert v == ['q', 4, 1]
v = []
del obj.vproperty
assert v == ['del', 5, 1]
# verify that defer correctly establishes exception chain (even on py2).
def test_defer_excchain():
# just @func/raise embeds traceback and adds ø chain
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment