Search code examples
pythonpython-3.xpython-2.7python-decoratorspython-object

how to get the attribute of setter method of property in python


Please consider the below code

class DataMember():
  def __init__(self, **args):
     self.default = {"required" : False , "type" : "string" , "length": -1}
     self.default.update(args)
  def __call__(self , func):
     #Here I want to set the attribute to method 
     setattr(func , "__dbattr__" , self.default)
     def validate(obj , value):
        #some other code
        func(obj , value)
     return validate

This is my decorator method to decorate the setter method of property of other class, I want to set some attribute to the method. but it doesn't allow me.

I tried as below

class User(DbObject):
  def __init__(self):
      super(User , self)
      self._username = None
  @property
  def Name(self):
      return self._username

  @Name.setter
  @DataMember(length=100)
  def Name(self , value):
      self._username = value

 u = User()
 u.Name = "usernameofapp"
 print(u.Name)
 print(u.Name.__dbattr__)

Got the below error when ran this

Traceback (most recent call last):
File "datatypevalidator.py", line 41, in <module>
print(u.Name.__dbattr__)
AttributeError: 'str' object has no attribute '__dbattr__'

What am I doing wrong, and how can I set some attribute to setter method.


Solution

  • OK so there are three points of confusion here. Object identity, descriptor protocols and dynamic attributes.

    First off, you are assigning __dbattr__ to func.

    def __call__(self , func): 
        func.__dbattr__ = self.default  # you don't need setattr
        def validate(obj , value):
            func(obj , value)
        return validate
    

    But this is assigning the attribute to func, which is then only held as a member of validate which in turn replaces func in the class (this is what decorators do ultimately, replace one function with another). So by placing this data on func, we lose access to it (well without some serious hacky __closure__ access). Instead, we should put the data on validate.

    def __call__(self , func): 
        def validate(obj , value):
            # other code
            func(obj , value)
        validate.__dbattr__ = self.default
        return validate
    

    Now, does u.Name.__dbattr__ work? No, you still get the same error, but the data is now accessible. To find it, we need to understand python's descriptor protocol which defines how properties work.

    Read the linked article for a full explination but effectively, @property works by making an additional class with __get__, __set__ and __del__ methods which when you call inst.property what you actually do is call inst.__class__.property.__get__(inst, inst.__class__) (and similar for inst.property = value --> __set__ and del inst.property --> __del__(). Each of these in turn call the fget, fset and fdel methods which are references to the methods you defined in the class.

    So we can find your __dbattr__ not on u.Name (which is the result of the User.Name.__get__(u, User) but on the User.Name.fset method itself! If you think about it (hard), this makes sense. This is the method you put it on. You didn't put it on the value of the result!

    User.Name.fset.__dbattr__
    Out[223]: {'length': 100, 'required': False, 'type': 'string'}
    

    Right, so we can see this data exists, but it's not on the object we want. How do we get it onto that object? Well, it's actually quite simple.

    def __call__(self , func):
        def validate(obj , value):
            # Set the attribute on the *value* we're going to pass to the setter
            value.__dbattr__ = self.default
            func(obj , value)
        return validate
    

    This only works if ultimately the setter returns the value, but in your case it does.

    # Using a custom string class (will explain later)
    from collections import UserString
    
    u = User()
    u.Name = UserString('hello')
    u.Name # --> 'hello'
    u.Name.__dbattr__  # -->{'length': 100, 'required': False, 'type': 'string'}
    

    You're probably wondering why I used a custom string class. Well if you use a basic string, you'll see the issue

    u.Name = 'hello'
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-238-1feeee60651f> in <module>()
    ----> 1 u.Name = 'hello'
    
    <ipython-input-232-8529bc6984c8> in validate(obj, value)
          6 
          7         def validate(obj , value):
    ----> 8             value.__dbattr__ = self.default
          9             func(obj , value)
         10         return validate
    
    AttributeError: 'str' object has no attribute '__dbattr__'
    

    str objects, like most in-built types in python, do not allow random attribute assignment like custom python classes (collections.UserString is a python class wrapper around string that does allow random assignment).

    So ultimately, what you originally wanted was impossible with built-in strings but using a custom class allows you to do it.