3

I have the following Python metaclass that adds a deco_with_args decorator to each class:

def deco_with_args(baz):
    def decorator(func):
        ...
        return func
    return decorator

class Foo(type):
    def __prepare__(name, bases):    
        return {'deco_with_args': deco_with_args}

This allows me to use the decorator like this:

class Bar(metaclass=Foo):
    @deco_with_args('baz')
    def some_function(self):
        ...

How do I make the deco_with_args decorator behave like an @classmethod so that I can access the Bar class (or whatever other class) from within the decorator function?

I have tried using @classmethod on the deco_with_args function with no luck.

8
  • Are you sure you want to use a metaclass just to define a decorator? Commented Mar 17, 2019 at 13:58
  • 2
    @DeepSpace There are reasons for it. It's a follow-up for this question. Commented Mar 17, 2019 at 14:01
  • Welp, I just realized that this isn't possible. Time to rewrite my 60%-finished answer... Commented Mar 17, 2019 at 14:12
  • To clarify: Do you need access to the class in test or in decorator? Commented Mar 17, 2019 at 14:13
  • Just inside decorator. Commented Mar 17, 2019 at 14:16

3 Answers 3

4

There are two interpretations on your question - if you need cls to be available when the function named decorator in your example is called (i.e. you need your decorated methods to become class methods), it suffices that itself is transformed into a classmethod:

def deco_with_args(baz):
    def decorator(func):
        ...
        return classmethod(func)
    return decorator

The second one is if you need cls to be available when deco_with_args itself is called, when creating the decorated function itself, at class creation. The answer that is listed as accepted right now lists the straightforward problem with that: The class does not exist yet when the class body is run, so, there is no way that at the end of parsing the class body you can have methods that would have known of the class itself.

However, unlike that answer tries to imply, that is not a real deal. All you have to do is to run your decorator code (the code that needs the cls) lazily, at the end of the class creation process. You already have a metaclass setup, so doing this is almost trivial, by just adding another callable layer around your decorator-code:

def deco_with_args(baz):
    def outter_decorator(func):
        def decorator(cls):
            # Code that needs cls at class creation time goes here
            ...

            return func
        return decorator
    outter_decorator._deco_with_args = True
    return outter_decorator

class Foo(type):
    def __prepare__(name, bases):    
        return {'deco_with_args': deco_with_args}

    def __init__(cls, cls_name, bases, namespace, **kwds):
        for name, method in cls.__dict__.items():
            if getattr(method, '_deco_with_args', False):
                cls.__dict__[name] = method(cls)

        super().__init__(cls_name, bases, namespace, **kwds)

This will be run, of course, after the class body execution is complete, but before any other Python statement after the class is run. If your decorator would affect other elements that are executed inside the class body itself, all you need to do is to wrap those around to warrant a lazy-execution as well.

Sign up to request clarification or add additional context in comments.

5 Comments

Still broken. decorator is never executed. outter_decorator._deco_with_args = True needs to be changed to decorator._deco_with_args = True and cls.__dict__[name] = method(cls) needs to be setattr(cls, name, method(cls)).
This is more what I was looking for. Is it right for me to change the accepted answer?
I think so - I even commented of the possibility with the author of the original answer, and he expressed that he preferred to keep it that way. So, if this works better, it makes sense for this to be accepted.
I know I'm probably getting on your nerves at this point, but why didn't you fix that one last bug?
It was about having two "name" variables, right? I did not fix it when you spotted, but yes, it is fixed by now.
2

@classmethod does nothing useful for your decorator because it's not invoked through a class or instance. classmethod is a descriptor, and descriptors only take effect on attribute access. In other words, it would only help if the decorator was called like @Bar.deco_with_args('baz').

The next problem is that the class does not exist yet at the time the decorator is executed. Python executes all of the code in the function body before creating the class. So it's impossible to access the class in deco_with_args or decorator.

4 Comments

"impossible" things are exactly what metaclasses where designed to overcome.
@jsbueno Well, it's true that whatever the OP is trying to achieve can probably be done with a metaclass. But it's undeniable that there's no way to access the class within this decorator. I'm not sure if I want to expand my answer to include a generic "You can write a decorator that marks functions and then post-process the marked functions in your metaclass's __init__" hack that people will likely use to create horrible code. 9 times out of 10, there's no real need for something like that.
@Aran-Fey I actually wanted access to the class in order to mark the functions: cls.marked_functions.append(func). How would you do it without the class?
@DavidCallanan I'd just set an attribute like func.marked = True.
0

You can use the descriptor protocol to capture your calls to the method and add the class as parameter on the fly:

def another_classmethod(baz):

  class decorator:
    def __init__(self, func):
      self.func = func
    def __get__(self, instance, owner):
      def new_call(*args, **kwargs):
        print(baz, self.func(owner, *args, **kwargs))
      return new_call

  return decorator


class Bar():
    @another_classmethod('baz')
    def some_function(cls):
        return f"test {cls.__name__}"

Bar.some_function()

This prints:

baz test Bar

The main "trick" here is that the protocol when calling Bar.some_function() is to first call __get__ then __call__ on the function returned by __get__.

Note that __get__ is also called when you just do Bar.some_function that's what is used in decorators like @property.

One small remark, when using classmethod you are not supposed to name your first parameter self as it is confusing (it would make people think that the first parameter is an instance instead of a class object/type).

5 Comments

I don't think the OP wanted to turn some_function into a classmethod. That would be easy. Just slap on a @classmethod and you're done. Implementing your own descriptor for this looks like overkill to me.
That's what the question says, I'm not even reading between the lines "How do I make the deco_with_args decorator behave like an @classmethod"
My point exactly. It doesn't say "How do I make some_function behave like a classmethod".
I don't get your point, the goal here is to have a decorator, that behave like classmethod, with arguments added to the decorator .
It's not overkill as the classmethod decorator takes no parameter and even if it did it wouldn't call something like print(baz, func()).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.