venusian
venusian copied to clipboard
Metaclass decoration breaks venusian scope/frame detection mechanism
I am writing a somewhat complex application that uses metaclasses to decorate functions (specifically, add views for a pyramid web application). However, when decorating inside a metaclass, venusian breaks.
From looking at the source, the reason for this seems to be the detection of scope that assumes I must be in a function call (which is correct) and therefore am not decorating a class method (which is incorrect).
I have never worked with venusian before but I tried to figure out the source code anyway. However, at this point I had to give up: I don't exactly understand what's going on here, so I couldn't figure out a fix. Instead, I have created a small testcase that replicates this issue:
import venusian
def test_decorator(wrapped):
print("Decorated")
def _callback(context, name, ob):
print("Callback called")
venusian.attach(wrapped, _callback)
return test_decorator
class TestMeta(type):
def __init__(self, name, bases, attrs):
self.test = test_decorator(self.test)
class Test:
__metaclass__ = TestMeta
#@test_decorator
def test(self):
print("test called")
This is, in essence, the problem: The decoration happens during the TestMeta.__init__
method (thus it is a function call) but the method being decorated is at the class scope.
I have tried playing with the depth parameter but I cannot get it to a class level (likely because of the way metaclasses work). If this is not considered a bug, please let me know how to work around it.
Note: You can check the correct way by removing the comment for the decorator above test
and commeting out the __metaclass__
attribute instead:
class Test:
#__metaclass__ = TestMeta
@test_decorator
def test(self):
print("test called")
This is probably a bit late, but for reference, it should be possible to achieve this by invoking the API slightly differently. Your example isn't working because of how Venusian introspects the stack frame to determine the calling context. I'll go through this.
Venusian has two main functions: attach
and scan
. scan
works by looking at all the top-level objects on a module, checking whether each one has a certain property ("venusian_callbacks" == venusian.ATTACH_ATTR at the time of this writing), and if so, getting the value of that property as a list of callbacks to execute.
attach
, on the other hand, works in a couple of different way, one for each way a decorator could be called (although the second and third cases are the same here). attach
can technically be applied to anything, of course, but is meant for decorating
- Methods of classes which are at the top-level of a module (but I'm guessing only when declared directly inside of a class block)
- Functions which are visible at the top-level of a module
- Classes which are visible at the top-level of a module (same as 2)
The reason why
class Test:
#__metaclass__ = TestMeta
@test_decorator
def test(self):
print("test called")
works is because it is an example of 1. In this case, Venusian "cheats" slightly and puts "venusian_callbacks" on Test
with a reference to test(self)
. This is done so that the API looks the same for the purpose of using decorators, even though it's operating differently under the hood.
Calling
class TestMeta(type):
def __init__(self, name, bases, attrs):
self.test = test_decorator(self.test)
doesn't work because it's no longer a case of 1. Case 1 only applies in the "top level" of a class declaration block. Right now, this scope is inside a function (inside a class declaration). Therefore, Venusian will simply put both "__venusian_callbacks__"
and a reference to test(self)
onto test(self)
, which defeats the purpose -- test(self)
isn't visible at the top-level of a module because it's inside Test
.
A solution I came up with would be to modify test_decorator
and __init__
slightly. Instead of applying test_decorator
to a method of Test
, apply it directly to Test
, which will be visible at the top-level of the module (case 3). Then, adjust your code to handle the fact that Test
is decorated instead of test(self)
. Something like
def test_decorator(wrapped):
...
if isinstance(wrapped, type): # check whether it's Test
venusian.attach(wrapped, _callback, depth=2)
...
class TestMeta(type):
def __init__(cls, name, bases, atts):
if name != TestMeta.__name__:
test_decorator(cls)
...
For reference, see the internal check for method vs. class/function scoping. https://github.com/Pylons/venusian/blob/e2d5d32ddbed62c7b4baaf1bd138cb85b0e4e4f4/venusian/init.py#L302-322