0

I have a self-written class-based Python decorator. As far as I can see there is a difference, when I apply the decorator to a method in a class. Usually, decorators like @classmethod, @staticmethod, @property or @unique are applied without parenthesis. These decorators expect no parameters and are (mostly?) written as function-based decorators.

So in contrast to these examples, my decorator is class-based and expects an optional parameter while applying.

My decorator:

class DocumentMemberAttribute(Attribute):
  def __init__(self, value=True):
    super().__init__()
    self.value = value

The Attributes class (the real decorator):

class Attribute:
  __AttributesMemberName__ = "__pyattr__"

  def __call__(self, func):
    self._AppendAttribute(func, self)
    return func

  @staticmethod
  def _AppendAttribute(func, attribute):
    # inherit attributes and append myself or create a new attributes list
    if (Attribute.__AttributesMemberName__ in func.__dict__):
      func.__dict__[Attribute.__AttributesMemberName__].append(attribute)
    else:
      func.__setattr__(Attribute.__AttributesMemberName__, [attribute])

  def __str__(self):
    return self.__name__

Example class:

class MyClass:
  def __init__(self, message=""):
    super().__init__()
    self.message = message

  @DocumentMemberAttribute
  def Method1(self):
    return "foo"

  @DocumentMemberAttribute()
  def Method2(self):
    return "bar"

  @DocumentMemberAttribute(False)
  def Method3(self):
    return "spam"

  @DocumentMemberAttribute(True)
  def Method4(self):
    return "egg"

The attached information is used in a custom autodoc-skip-member handler to decide if a method shall be documented or skipped. This is similar to the docit extension.

So when we look at the generated documentation (Sphinx), we can see these results:

class MyClass(message="")

Method1 = <lib.SphinxExtensions.DocumentMemberAttribute object at 0x0000000004AD9E80>

Method2()

Method4()

What can we see here:

  • Method1 has no parenthesis indicating a function/method, hence it's regarded a class field and Sphinx uses __str__ (or __repr__?) to document the initial value
  • Method3 is not documented as intended.

So my questions:

  • Why is there a difference in the behavior?
  • MUST I apply class-based attributes with parenthesis?
  • How should a user know how to use these 2 kinds of decorators? (I can document it for myself, but others might remember just the name und forget to add the parenthesis, because other decorators don't require them.)

pyAttributes is a set of attributes (real attributes, no Python attributes) written by me. They behave almost like in .NET

Paebbels
  • 15,573
  • 13
  • 70
  • 139
  • as with most decoration definitions, the `@test` is equivalent to `@Test()` meaning, no parameters. For example, I use decorators in the language *Dart* because it needs to leverage them to interact with front end markup. I had to do a lot of research into annotations and their purposes and how/what they are used for – Fallenreaper Nov 20 '16 at 02:09
  • 2
    Decorators evaluate an expression (the one following the `@`) and call the result of that expression, passing the decorated item as an argument and replacing it with the returned value. `func` is different from `func()`, so `@func` is different from `@func()`. – Ry- Nov 20 '16 at 02:16
  • http://click.pocoo.org/5/ for a way of doing what was described in your README without trying to fit .NET idioms into Python, by the way. – Ry- Nov 20 '16 at 02:19
  • @Ryan Yes, `@deco \n class foo:` gets translated to `class foo: \n foo = deco(foo)`. I noticed that PyCharm colors function-based decorators in blue and class-based decorators in red. Other then this, how should a user know how to call/apply a decorator? – Paebbels Nov 20 '16 at 02:36
  • @Ryan Or do you mean this translation: `@deco() \n class foo:` gets translated to `class foo: \n foo = deco()(foo)` ?? – Paebbels Nov 20 '16 at 02:37
  • 1
    Yes. `@expr class foo:` is `foo = (expr)(foo)` for any `expr`, so `deco()(foo)` is correct. – Ry- Nov 20 '16 at 02:42
  • Can you write up this comment as an answer. – Paebbels Nov 20 '16 at 02:46
  • @Ryan pyAttributes implements attributes in general. The package is shipped with a set of attributes for argparse, which allow a nicer interface to apply argparse parameters directly to the handler method. – Paebbels Nov 20 '16 at 02:52

1 Answers1

0

As @Ryan pointed out, the string behind the @ sign is an expression, which gets translated to a function call. The parameter of that call is the object, to which the decorator was applied.

Example 1 - function-based decorators:

I'll use the enum.unique decorator. It is written as a function.

from enum import Enum, unique

@unique
class MyEnum(Enum):
  foo = 0
  bar = 1

Gets translated to

from enum import Enum, unique

class MyEnum(Enum):
  foo = 0
  bar = 1

MyEnum = unique(MyEnum)

Example 2 - class-based decorators:

I'll use the decorator from the question. It is written as a class.

class MyClass:
  @DocumentMemberAttribute()
  def Method1():
    pass

  @DocumentMemberAttribute(True)
  def Method2():
    pass

Gets translated to

class MyClass:
  def Method1():
    pass

  Method1 = DocumentMemberAttribute()(Method1)

  def Method2():
    pass

  Method2 = DocumentMemberAttribute(True)(Method2)

Please note the empty parenthesis before passing Method1 as an argument to the class' __call__ method. These are the initializer's (__init__) parenthesis. So these parenthesis get filled for Method2 when passing True as an argument.

So in conclusion:

  • function-based decorators are applied without parenthesis
  • class-based decorators are applied with parenthesis

Note for PyCharm users:

Look at the colored @ sign:

  • blue -> function-based decorator
  • red -> class-based decorator
Paebbels
  • 15,573
  • 13
  • 70
  • 139