Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metaclass decoration breaks venusian scope/frame detection mechanism #32

Open
Javex opened this issue Mar 10, 2014 · 1 comment
Open

Comments

@Javex
Copy link

Javex commented Mar 10, 2014

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")
@Artiavis
Copy link

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

  1. 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)
  2. Functions which are visible at the top-level of a module
  3. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants