Commit 97264b07 authored by Kirill Smelkov's avatar Kirill Smelkov

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

This is take 2 after 924a808c (golang: Fix `@func(cls) def name` not to
override `name` in calling context). There we fixed it not to override
name if name was already set, but for the case of unset name it was
still set. The following example was thus not working correctly as
builtin `next` was overridden:

    class BitSync

    @func(BitSync)
    def next(): ...         # this was shadowing access to builtin next

    def peek(seq):
        return next(...)    # here next was taken not from builtin, but
                            # from result of above shadowing

To solve the problem in the patch from 2019 I initially contemplated
patching bytecode because python unconditionally does STORE_NAME after a
function is defined with decorator:

    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

However after hitting this problem for real again and taking a fresh
look I found a way to arrange for the good end result without bytecode
magic: if name is initially unset @func can install its own custom
object, which, when overwritten by normal python codeflow of invoking
STORE_NAME after decorator, unsets the attribute.

That works quite ok and the patch with the fix is small.

/cc @jerome
parent 0d53a6d1
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (C) 2018-2023 Nexedi SA and Contributors. # Copyright (C) 2018-2024 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com> # Kirill Smelkov <kirr@nexedi.com>
# #
# This program is free software: you can Use, Study, Modify and Redistribute # This program is free software: you can Use, Study, Modify and Redistribute
...@@ -85,11 +85,29 @@ def _meth(cls, fcall): ...@@ -85,11 +85,29 @@ def _meth(cls, fcall):
if already is not missing: if already is not missing:
return already return already
# FIXME try to arrange so that python does not set anything on caller's # arrange so that python eventually does not set anything on caller's
# namespace[func_name] (currently it sets that to implicitly returned None) # namespace[func_name] (it unconditionally sets what decorator returns, even implicit None)
#
# _DelAttrAfterMeth.__del__ is invoked:
# * on cpython: right after namespace[func_name] = returned _meth_leftover
# * on pypy: eventually on next GC
fcall.f_locals[func_name] = _DelAttrAfterMeth(fcall.f_locals, func_name)
return _meth_leftover
return deco return deco
# _DelAttrAfterMeth serves _meth by unsetting f_locals[meth] that python
# unconditionally sets after `@func(cls) def meth()`.
_meth_leftover = object()
class _DelAttrAfterMeth(object):
def __init__(self, f_locals, name):
self.f_locals = f_locals
self.name = name
def __del__(self):
obj = self.f_locals.get(self.name)
if obj is _meth_leftover:
del self.f_locals[self.name]
# _func serves @func. # _func serves @func.
def _func(f): def _func(f):
# @staticmethod & friends require special care: # @staticmethod & friends require special care:
......
...@@ -1004,12 +1004,20 @@ def test_func(): ...@@ -1004,12 +1004,20 @@ def test_func():
assert mcls is mcls_orig assert mcls is mcls_orig
assert mcls == 'mcls' assert mcls == 'mcls'
# FIXME undefined var after `@func(cls) def var` should be not set # undefined var after `@func(cls) def var` should be not set
assert 'var' not in locals()
@func(MyClass)
def var(self, v):
assert v == 8
return v + 1
gc.collect() # pypy needs this to trigger _DelAttrAfterMeth GC
assert 'var' not in locals()
obj = MyClass(4) obj = MyClass(4)
assert obj.zzz(4) == 4 + 1 assert obj.zzz(4) == 4 + 1
assert obj.mstatic(5) == 5 + 1 assert obj.mstatic(5) == 5 + 1
assert obj.mcls(7) == 7 + 1 assert obj.mcls(7) == 7 + 1
assert obj.var(8) == 8 + 1
# this tests that @func (used by @func(cls)) preserves decorated function signature # this tests that @func (used by @func(cls)) preserves decorated function signature
assert fmtargspec(MyClass.zzz) == '(self, v, x=2, **kkkkwww)' assert fmtargspec(MyClass.zzz) == '(self, v, x=2, **kkkkwww)'
...@@ -1023,6 +1031,8 @@ def test_func(): ...@@ -1023,6 +1031,8 @@ def test_func():
assert MyClass.mcls.__module__ == __name__ assert MyClass.mcls.__module__ == __name__
assert MyClass.mcls.__name__ == 'mcls' assert MyClass.mcls.__name__ == 'mcls'
assert MyClass.var.__module__ == __name__
assert MyClass.var.__name__ == 'var'
# @func overhead at def time. # @func overhead at def time.
def bench_def(b): def bench_def(b):
......
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