how to get the attribute of setter method of property in python
Asked Answered
H

3

7

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.

Haro answered 26/8, 2018 at 10:7 Comment(2)
@LevZakharov, There would be more property in User class and can have different DataMember decorator parameters, Then how I will access the attributes for each property separately, Please explain, I am a bit low in pythonHaro
Could you remove the dependency on DbObject so one can actually run the example?Sumerian
O
8

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.

Overturn answered 28/8, 2018 at 11:47 Comment(1)
Perfect and well explained answer.Haro
L
4

access __dbattr__ is a bit tricky:

first, you need get the property object:

p = u.__class__.__dict__['Name']

then get back the setter function object, named validate, which is defined inside DataMember.__call__:

setter_func = p.fset

then get back the underlying User.Name(self , value) function object from the closure of setter_func:

ori_func = setter_func.__closure__[0].cell_contents

now you could access __dbattr__:

>>> ori_func.__dbattr__
{'required': False, 'type': 'string', 'length': 100}

but is that useful? I don't know. maybe you could just set __dbattr__ on the validate function object that returned by DataMember.__call__, as other answers have pointed out.

Leverick answered 28/8, 2018 at 11:47 Comment(0)
S
4

You need to set the attribute on the wrapper function that is being returned by your decorator class's call method:

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
     def validate(obj , value):
        #some other code
        func(obj , value)
     setattr(validate , "__dbattr__" , self.default)
     return validate

class DbObject: pass

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

But note, it is not a method, since there is a property on the class, it instances will only ever return a string, the one returned by the getter. To access the setter, you have to do it indirectly through the property, which is on the class:

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

which prints:

usernameofapp
{'required': False, 'type': 'string', 'length': 100}
Soudan answered 28/8, 2018 at 11:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.