how to get the attribute of setter method of property in python
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.
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.
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}