How to cancel class decorators in constructor - python

Lets take the following example of class decorators (origin http://www.informit.com/articles/article.aspx?p=1309289&seqNum=4):
class GenericDescriptor:
def __init__(self, getter, setter):
self.getter = getter
self.setter = setter
def __get__(self, instance, owner=None):
if instance is None:
return self
return self.getter(instance)
def __set__(self, instance, value):
return self.setter(instance, value)
def valid_string(attr_name, empty_allowed=True, regex=None,
acceptable=None):
def decorator(cls):
name = "__" + attr_name
def getter(self):
return getattr(self, name)
def setter(self, value):
assert isinstance(value, str), (attr_name +
" must be a string")
if not empty_allowed and not value:
raise ValueError("{0} may not be empty".format(
attr_name))
if ((acceptable is not None and value not in acceptable) or
(regex is not None and not regex.match(value))):
raise ValueError("{attr_name} cannot be set to "
"{value}".format(**locals()))
setattr(self, name, value)
setattr(cls, attr_name, GenericDescriptor(getter, setter))
return cls
return decorator
#valid_string("name", empty_allowed=False)
class StockItem:
name = None
def __init__(self, **kwargs):
if kwargs.get('second_call'):
pass
# proceed normally without calling #valid_string
self.name = kwargs.get('name', None)
self.price = kwargs.get('price', None)
self.quantity = kwargs.get('quantity', None)
if __name__ == "__main__":
import doctest
doctest.testmod()
# valid value for name
cameras1 = StockItem(name="Camera", price=45.99, quatity=2)
# invalid value for name according to #valid_string but I need this to be also valid if 'second_call'
cameras2 = StockItem(name=67, price=45.99, quatity=2, second_call=True)
The StockItem class constructor is invoked twice and on the second turn I want the #valid_string decorator to be somehow canceled (I don't want name attribute's value to be altered anymore).

Related

Check a type attribute with a Descriptor and a Decorator : "__get__() takes 2 positional arguments but 3 were given" when accessing the attribute

Goal
I try to implement a Decorator that could act like a typechecking function. I'm not there yet since the validation is working well but when I try to access the attribute I get the error : TypeError: __get__() takes 2 positional arguments but 3 were given.
I'm using Python 3.9.7.
Description
# Descriptor definition
class PositiveNumber:
def __get__(self, obj):
return self.value
def __set__(self, obj, value):
if not isinstance(value, (int, float)):
raise TypeError('value must be a number')
if value < 0:
raise ValueError('Value cannot be negative.')
self.value = value
# Decorator definition
def positive_number(attr):
def decorator(cls):
setattr(cls, attr, PositiveNumber())
return cls
return decorator
# class that actually check for the right type
#positive_number('number')
class MyClass(object):
def __init__(self, value):
self.number = value
def main():
a = MyClass(3)
print(a.number)
if __name__ == '__main__':
main()
Do you see the way to fix this ?
You got the signature wrong. When the __get__ is called, 3 arguments are passed (including the type whether you use it or not):
class PositiveNumber:
def __get__(self, obj, objtype=None):
return self.value
# ...
The other thing that is wrong: there is only one descriptor instance, so all instances of MyClass will share that object's value attribute!
a = MyClass(3)
b = MyClass(5)
a.number
# 5
So you better fix this associating the set values with the instances:
class PositiveNumber:
def __get__(self, obj, objtype=None):
return obj._value
def __set__(self, obj, value):
if not isinstance(value, (int, float)):
raise TypeError('value must be a number')
if value < 0:
raise ValueError('Value cannot be negative.')
obj._value = value
You could also use a more specific attribute name in accordance with the name of the field on the actual class:
class PositiveNumber:
def __set_name__(self, owner, name):
self.private_name = '_' + name # e.g. "_number"
def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)
def __set__(self, obj, value):
if not isinstance(value, (int, float)):
raise TypeError('value must be a number')
if value < 0:
raise ValueError('Value cannot be negative.')
setattr(obj, self.private_name, value)
The set_name method is only called when the descriptor is already there upon class creation. That means you cannot set it dynamically with setattr on the already existing class object, but you have to create a new class!
def positive_number(attr):
def decorator(cls):
return type(cls.__name__, (cls,), {attr: PositiveNumber()})
return decorator

Class decorator with parameters

MyClass = (decorator(params))(MyClass)
I don't understand what this code means, since a decorator would return a function then MyClass would become a function? That sounds weird, isn't MyClass supposed to stay a class?
EDIT:
def decorator(name, validate, doc=None):
def dec(Class):
privateName = "__" + name
def getter(self):
return getattr(self, privateName)
def setter(self, value):
validate(name, value)
setattr(self, privateName, value)
setattr(Class, name, property(getter, setter, doc=doc))
return Class
return dec

Python descriptor for type checks and immutability

Read the Python Cookbook and saw descriptors, particularly the example for enforcing types when using class attributes. I am writing a few classes where that would be useful, but I would also like to enforce immutability. How to do it? Type checking descriptor adapted from the book:
class Descriptor(object):
def __init__(self, name=None, **kwargs):
self.name = name
for key, value in kwargs.items():
setattr(self, key, value)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
# by default allows None
class Typed(Descriptor):
def __init__(self, expected_types=None, **kwargs):
self.expected_types = expected_types
super().__init__(**kwargs)
def __set__(self, instance, value):
if value is not None and not isinstance(value, self.expected_types):
raise TypeError('Expected: {}'.format(str(self.expected_types)))
super(Typed, self).__set__(instance, value)
class T(object):
v = Typed(int)
def __init__(self, v):
self.v = v
Attempt #1: add a self.is_set attribute to Typed
# by default allows None
class ImmutableTyped(Descriptor):
def __init__(self, expected_types=None, **kwargs):
self.expected_types = expected_types
self.is_set = False
super().__init__(**kwargs)
def __set__(self, instance, value):
if self.is_set:
raise ImmutableException(...)
if value is not None and not isinstance(value, self.expected_types):
raise TypeError('Expected: {}'.format(str(self.expected_types)))
self.is_set = True
super(Typed, self).__set__(instance, value)
Wrong, because when doing the following, ImmutableTyped is 'global' in the sense that it's a singleton throughout all instances of the class. When t2 is instantiated, is_set is already True from the previous object.
class T(object):
v = ImmutableTyped(int)
def __init__(self, v):
self.v = v
t1 = T()
t2 = T() # fail when instantiating
Attempt #2: Thought instance in __set__ refers to the class containing the attribute so tried to check if instance.__dict__[self.name] is still a Typed. That is also wrong.
Idea #3: Make Typed be used more similar to #property by accepting a 'fget' method returning the __dict__ of T instances. This would require the definition of a function in T similar to:
#Typed
def v(self):
return self.__dict__
which seems wrong.
How to implement immutability AND type checking as a descriptor?
Now this is my approach to the problem:
class ImmutableTyped:
def __set_name__(self, owner, name):
self.name = name
def __init__(self, *, immutable=False, types=None)
self.immutable == immutable is True
self.types = types if types else []
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if self.immutable is True:
raise TypeError('read-only attribute')
elif not any(isinstance(value, cls)
for cls in self.types):
raise TypeError('invalid argument type')
else:
instance.__dict__[self.name] = value
Side note: __set_name__ can be used to allow you to not specify the attribute name in initialisation. This means you can just do:
class Foo:
bar = ImmutableTyped()
and the instance of ImmutableTyped will automatically have the name attribute bar since I typed for that to occur in the __set_name__ method.
Could not succeed in making such a descriptor. Perhaps it's also unnecessarily complicated. The following method + property use suffices.
# this also allows None to go through
def check_type(data, expected_types):
if data is not None and not isinstance(data, expected_types):
raise TypeError('Expected: {}'.format(str(expected_types)))
return data
class A():
def __init__(self, value=None):
self._value = check_type(value, (str, bytes))
#property
def value(self):
return self._value
foo = A()
print(foo.value) # None
foo.value = 'bla' # AttributeError
bar = A('goosfraba')
print(bar.value) # goosfraba
bar.value = 'bla' # AttributeError
class ImmutableTyped(object):
def __set_name__(self, owner, name):
self.name = name
def __init__(self, *, types=None):
self.types = tuple(types or [])
self.instances = {}
return None
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
is_set = self.instances.setdefault(id(instance), False)
if is_set:
raise AttributeError("read-only attribute '%s'" % (self.name))
if self.types:
if not isinstance(value, self.types):
raise TypeError("invalid argument type '%s' for '%s'" % (type(value), self.name))
self.instances[id(instance)] = True
instance.__dict__[self.name] = value
return None
Examples:
class Something(object):
prop1 = ImmutableTyped(types=[int])
something = Something()
something.prop1 = "1"
Will give:
TypeError: invalid argument type '<class 'str'>' for 'prop1'
And:
something = Something()
something.prop1 = 1
something.prop1 = 2
Will give:
TypeError: read-only attribute 'prop1'

Overwritten __repr__ after instance creation

I am playing a little bit around with Python metaprogramming.
class FormMetaClass(type):
def __new__(cls, clsname, bases, methods):
# Attach attribute names to the descriptors
for key, value in methods.items():
if isinstance(value, FieldDescriptor):
value.name = key
return type.__new__(cls, clsname, bases, methods)
class Form(metaclass=FormMetaClass):
#classmethod
def from_json(cls, incoming):
instance = cls()
data = json.loads(incoming)
for k, v in data.items():
if (not hasattr(instance, k)):
raise KeyError("Atrribute not found")
instance.__setattr__(k, v)
return cls
class MyForm(Form):
first_name = String()
last_name = String()
age = Integer()
def __repr__(self):
return "{} {}".format(self.first_name, self.last_name)
def main():
data = json.dumps({'first_name': 'Thomas',
'last_name': 'Junk'})
form = MyForm.from_json(data)
print(form)
if __name__ == "__main__":
main()
class FieldDescriptor:
def __init__(self, name=None, **opts):
self.name = name
for key, value in opts.items():
setattr(self, key, value)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
class Typechecked(FieldDescriptor):
expected_type = type(None)
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError('expected ' + str(self.expected_type))
super().__set__(instance, value)
class Integer(Typechecked):
expected_type = int
class String(Typechecked):
expected_type = str
I have a Form which has a metaclass FormMetaClass.
To have an alternative constructor I am using a #classmethod.
I create an instance, which seem to work so far.
What doesn't work is calling the __repr__ (or __str__ interchangeably).
When I create an instance via MyForm() everything is fine.
When I create an instance via the #classmethod, some "default" implementation is taken.
I expected Thomas Junk, but I get <class '__main__.MyForm'>
Could you give me a hint, what I am overlooking?
You are returning the class, not the newly created instance:
return cls
So you return MyForm, not the new instance MyForm() you just set all the attributes on. And you indeed see the repr() output for the class:
>>> form is MyForm
True
>>> print(MyForm)
<class '__main__.MyForm'>
The fix is simple, return instance instead:
return instance
or, as a full method:
#classmethod
def from_json(cls, incoming):
instance = cls()
data = json.loads(incoming)
for k, v in data.items():
if (not hasattr(instance, k)):
raise KeyError("Atrribute not found")
instance.__setattr__(k, v)
return instance
at which point the method returns an instance and everything works:
>>> isinstance(form, MyForm)
True
>>> print(form)
Thomas Junk

Determine if class attribute is a read-only data descriptor

A read-only data descriptor is a descriptor that defines both __get__ and __set__, but __set__ raises AttributeError when called.
An example is a simple read-only property:
class Test():
_i = 1
#property
def i(self):
return self._i
assert hasattr(Test.i, '__get__')
assert hasattr(Test.i, '__set__')
t = Test()
t.i # 1
t.i = 2 # ERROR
If I have an instance of a class, I can determine if the instance attribute is a read-only data descriptor this way (although I don't like this at all):
def is_ro_data_descriptor_from_instance(instance, attr):
temp = getattr(instance, attr)
try:
setattr(instance, attr, None)
except AttributeError:
return True
else:
setattr(instance, attr, temp)
return False
If I know the class doesn't require any arguments to be instantiated, I can determine if its class attribute is a read-only data descriptor similar to the above:
def is_ro_data_descriptor_from_klass(klass, attr):
try:
setattr(klass(), attr, None)
except AttributeError:
return True
else:
return False
However, if I don't know the signature of the class ahead of time, and I try to instantiate a temporary object in this way, I could get an error:
class MyClass():
i = 1
def __init__(self, a, b, c):
'''a, b, and c are required!'''
pass
def is_ro_data_descriptor_from_klass(MyClass, 'i') # Error
What can be done to determine if a class attribute is a read-only data descriptor?
EDIT: Adding more information.
Below is the code I am trying to get working:
class StaticVarsMeta(type):
'''A metaclass that will emulate the "static variable" behavior of
other languages. For example:
class Test(metaclass = StaticVarsMeta):
_i = 1
#property
def i(self):
return self._i
t = Test()
assert t.i == Test.i'''
statics = {}
def __new__(meta, name, bases, dct):
klass = super().__new__(meta, name, bases, dct)
meta.statics[klass] = {}
for key, value in dct.items():
if "_" + key in dct:
meta.statics[klass][key] = set()
if hasattr(value, '__get__'):
meta.statics[klass][key].add('__get__')
if hasattr(value, '__set__'):
try:
value.__set__(None, None)
except AttributeError:
continue
else:
meta.statics[klass][key].add('__set__')
return klass
def __getattribute__(klass, attr):
if attr not in StaticVarsMeta.statics[klass]:
return super().__getattribute__(attr)
elif '__get__' not in StaticVarsMeta.statics[klass][attr]:
return super().__getattribute__(attr)
else:
return getattr(klass, '_' + attr)
def __setattr__(klass, attr, value):
if attr not in StaticVarsMeta.statics[klass]:
super().__setattr__(attr, value)
elif '__set__' not in StaticVarsMeta.statics[klass][attr]:
super().__setattr__(attr, value)
else:
setattr(klass, '_' + attr, value)
class Test(metaclass = StaticVarsMeta):
_i = 1
def get_i(self):
return self._i
i = property(get_i)
Note the following:
type(Test.i) # int
type(Test.__dict__['i']) # property
Test().i = 2 # ERROR, as expected
Test.i = 2 # NO ERROR - should produce an error
It seems super-awkward, but here's how you could implement it based on my comment:
class StaticVarsMeta(type):
statics = {}
def __new__(meta, name, bases, dct):
cls = super().__new__(meta, name, bases, dct)
meta.statics[cls] = {}
for key, val in dct.items():
if hasattr(val, '__get__') and hasattr(val, '__set__'):
meta.statics[cls][key] = {'__get__'}
try:
val.__set__(None, None)
except AttributeError as err:
if "can't set attribute" in err.args:
continue
meta.statics[cls][key].add('__set__')
return cls
In use:
>>> class ReadOnly(metaclass=StaticVarsMeta):
#property
def foo(self):
return None
>>> class ReadWrite(metaclass=StaticVarsMeta):
#property
def bar(self):
return None
#bar.setter
def bar(self, val):
pass
>>> StaticVarsMeta.statics
{<class '__main__.ReadOnly'>: {'foo': {'__get__'}},
<class '__main__.ReadWrite'>: {'bar': {'__get__', '__set__'}}}
This is more of a "starter for 10", there must be a better way to do it...
Your first solution can be made simpler and slightly more robust, by attempting to assign the value it already has. This way, no undoing is required (Still, this isn't thread-safe).
def is_ro_data_descriptor_from_instance(instance, attr):
temp = getattr(instance, attr)
try:
setattr(instance, attr, temp)
except AttributeError:
return True
else:
return False

Categories

Resources