Commit 924a808c authored by Kirill Smelkov's avatar Kirill Smelkov

golang: Fix `@func(cls) def name` not to override `name` in calling context

With @func being a decorator, the following

	@func(cls)
	def name():
		...

is always processed by python as

	name = func(cls)(def name(): ...)

Before this patch it was leading to name being overridden with None:

	def f():
		print 'hello'

	class C:
		pass

	@func(C)
	def f(c):
		print 'C.f', c

	f()
	Traceback (most recent call last):
	  File "<console>", line 1, in <module>
	TypeError: 'NoneType' object is not callable

We can fix it by returning from `func(cls)(def name(): ...)` the
original `name` object from the calling context.

Unfortunately if `name` was not previously set I did not find a way(*) to
avoid polluting the calling namespace where it is set to what @func(cls)
returns (None) by the hardcoded way how python processes decorators:

	In [2]: c = """
	   ...: @fff
	   ...: def ccc():
	   ...:     return 1
	   ...: """

	In [3]: cc = compile(c, "file", "exec")

	In [4]: dis(cc)
	  2           0 LOAD_NAME                0 (fff)
	              3 LOAD_CONST               0 (<code object ccc at 0x7fafe58d0130, file "file", line 2>)
	              6 MAKE_FUNCTION            0
	              9 CALL_FUNCTION            1
	             12 STORE_NAME               1 (ccc)	<-- NOTE means: ccc = what fff() call returns
	             15 LOAD_CONST               1 (None)
	             18 RETURN_VALUE

At least with no overriding taking place the situation is better now.

NOTE: it is only @func(cls) which potentially pollutes calling
namespace. Just @func (without class) is always clean because by
definition it works as a regular decorator.

(*) there is a very low-level and potentially fragile way to disable
STORE_NAME after CALL_FUNCTION by dynamically patching caller's bytecode
at runtime and replacing STORE_NAME with POP_TOP + NOP...
parent 08ec7950
......@@ -78,12 +78,13 @@ class _PanicError(Exception):
# ...
def func(f):
if inspect.isclass(f):
return _meth(f)
fcall = inspect.currentframe().f_back # caller's frame (where @func is used)
return _meth(f, fcall)
else:
return _func(f)
# _meth serves @func(cls).
def _meth(cls):
def _meth(cls, fcall):
def deco(f):
# wrap f with @_func, so that e.g. defer works automatically.
f = _func(f)
......@@ -93,6 +94,16 @@ def _meth(cls):
else:
func_name = f.__name__
setattr(cls, func_name, f)
# if `@func(cls) def name` caller already has `name` set, don't override it
missing = object()
already = fcall.f_locals.get(func_name, missing)
if already is not missing:
return already
# FIXME try to arrange so that python does not set anything on caller's
# namespace[func_name] (currently it sets that to implicitly returned None)
return deco
# _func serves @func.
......
......@@ -488,23 +488,34 @@ def test_method():
def __init__(self, v):
self.v = v
zzz = zzz_orig = 'z' # `@func(MyClass) def zzz` must not override zzz
@func(MyClass)
def zzz(self, v, x=2, **kkkkwww):
assert self.v == v
return v + 1
assert zzz is zzz_orig
assert zzz == 'z'
mstatic = mstatic_orig = 'mstatic'
@func(MyClass)
@staticmethod
def mstatic(v):
assert v == 5
return v + 1
assert mstatic is mstatic_orig
assert mstatic == 'mstatic'
mcls = mcls_orig = 'mcls'
@func(MyClass)
@classmethod
def mcls(cls, v):
assert cls is MyClass
assert v == 7
return v + 1
assert mcls is mcls_orig
assert mcls == 'mcls'
# FIXME undefined var after `@func(cls) def var` should be not set
obj = MyClass(4)
assert obj.zzz(4) == 4 + 1
......
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