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.
DbObject
so one can actually run the example? – Sumerian