Attrs dependant attributes on initialization - python

Let's say we have an attrs class:
#attr.s()
class Foo:
a: bool = attr.ib(default=False)
b: int = attr.ib(default=5)
#b.validator
def _check_b(self, attribute, value):
if not self.a:
raise ValueError("to define 'b', 'a' must be True")
if value < 0:
raise ValueError("'b' has to be a positive integer")
So the following behaviour is correct:
>>> Foo(a=True, b=10)
Foo(a=True, b=10)
>>> Foo(b=10)
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<attrs generated init __main__.Foo>", line 5, in __init__
__attr_validator_b(self, __attr_b, self.b)
File "<input>", line 9, in _check_b
ValueError: to define 'b', 'a' must be True
But this is not:
>>> Foo()
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<attrs generated init __main__.Foo>", line 5, in __init__
__attr_validator_b(self, __attr_b, self.b)
File "<input>", line 9, in _check_b
ValueError: to define 'b', 'a' must be True
This obviously happens because Foo.b is always initialized, regardless of when Foo.a is given value: via default value or on Foo.__init__.
Is there anyway to accomplish this attribute dependance with any of the initialization hooks?

Following #hynek's recommendation to have default values that result in a valid instance, I've changed default of the dependant attribute to None to validate only when a value is passed on __init__:
#attr.s()
class Foo:
a: bool = attr.ib(default=False)
b: Optional[int] = attr.ib(default=None)
#b.validator
def _check_b(self, attribute, value):
if value is None:
self.b = 5
return
if not self.a:
raise ValueError("to define 'b', 'a' must be True")
if value < 0:
raise ValueError("'b' has to be a positive integer")
I am aware that to change attributes's values on a validator is not optimal, but it gets the work done.
Documentation about validators can be found here

Depending what you want to do, you can check whether value is the default value (available on attribute.default). However, your whole problem is that the default values result in an instance that's, according to your validator, invalid. Thus there's probably better ways to model this.

Related

How to annotate an optional class variable

In Python 3 how do you add type annotations for a class variable that may or may not exist. This is distinct from a class variable that may be None. For example:
from typing import Optional
class Foo:
a: Optional[int]
# b: ???
def __init__(self, has_a: bool, has_b: bool):
if has_a:
self.a = 1
else
self.a = None
if has_b:
self.b = 2
Then
>>> x = Foo(False, False)
>>> repr(x.a)
'None'
>>> repr(x.b)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object has no attribute 'b'
What do I write in ??? to indicate to type checkers that b may be an int or it may not exist at all?

How to use __get__ and __set__ (python descriptors)

I'm new to using descriptors and I think I have a good understanding on how they work but I have come across a problem and i'm not sure how to fix it.
Code
class Foo:
class Bar:
def __get__(self,instance, owner):
return 10
def __set__(self,instance,value):
raise Exception
bar=Bar()
print(Foo.bar)
Foo.bar=5
print(Foo.bar)
Output
>>> 10
>>> 5
Im trying to make bar a constant for testing purposes, I know about the property decorator but I prefer using descriptors.
First I print out the value of bar to see if __get__ works - and it does, the output is 10.
But then when I assign 5 to bar the expected result would be an exception but instead what happens is 5 gets assigned to bar despite specifying __set__ so when I print again the second output is 5.
Can someone tell me why the error isn't being raised?
From the docs:
object.__set__(self, instance, value)
Called to set the attribute on an instance instance of the owner class to a new value, value.
In your code, Foo.bar = 5 is setting the class attribute, not an instance attribute. If you do use an instance (without first setting Foo.bar = 5, which overrides your descriptor), then you get an exception as expected:
>>> f = Foo()
>>> f.bar
10
>>> f.bar = 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in __set__
Exception
If you want the __set__ behaviour to apply when the class attribute is set, then the class itself needs to be an instance of a metaclass which uses the descriptor:
class FooMeta(type):
class Bar:
def __get__(self,instance, owner):
return 10
def __set__(self,instance,value):
raise Exception
bar = Bar()
class Foo(metaclass=FooMeta):
pass
Testing:
>>> Foo.bar
10
>>> Foo.bar = 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in __set__
Exception

ConfigParser with Custom Dict Type

I need to create a custom ConfigParser which adds a few features including, most relevantly here, a default value for unset keys of None. This seems to be supported through custom dict types and accordingly I wrote something like this:
class SyncDict(collections.UserDict):
...
def __getitem__(self, key):
if key in self.data:
return self.data[key]
return None
...
class SyncConfig(ConfigParser):
...
def __init__(self, filename):
super().__init__(allow_no_value=True, dict_type = SyncDict)
...
However, this does not work as it still raises KeyError in SectionProxy. For example,
>>> a = SyncConfig('aaa.cfg')
>>> a.add_section('b')
>>> b = a['b']
>>> b['c']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Program Files\Python38\lib\configparser.py", line 1254, in __getitem__
raise KeyError(key)
KeyError: 'c'
Am I missing something, or this really not supposed to be possible?
PS: Bonus points for a way to make SyncDict return a value of an empty SyncDict when asked for a section and None when asked for an option.

Setting Property via a String

I'm trying to set a Python class property outside of the class via the setattr(self, item, value) function.
class MyClass:
def getMyProperty(self):
return self.__my_property
def setMyProperty(self, value):
if value is None:
value = ''
self.__my_property = value
my_property = property( getMyProperty, setMyProperty )
And in another script, I create an instance and want to specify the property and let the property mutator handle the simple validation.
myClass = MyClass()
new_value = None
# notice the property in quotes
setattr(myClass, 'my_property', new_value)
The problem is that it doesn't appear to be calling the setMyProperty(self, value) mutator. For a quick test to verify that it doesn't get called, I change the mutator to:
def setMyProperty(self, value):
raise ValueError('WTF! Why are you not being called?')
if value is None:
value = ''
self.__my_property = value
I'm fairly new to Python, and perhaps there's another way to do what I'm trying to do, but can someone explain why the mutator isn't being called when setattr(self, item, value) is called?
Is there another way to set a property via a string? I need the validation inside the mutator to be executed when setting the property value.
Works for me:
>>> class MyClass(object):
... def get(self): return 10
... def setprop(self, val): raise ValueError("hax%s"%str(val))
... prop = property(get, setprop)
...
>>> i = MyClass()
>>> i.prop =4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in setprop
ValueError: hax4
>>> i.prop
10
>>> setattr(i, 'prop', 12)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in setprop
ValueError: hax12
The code you pasted seems to do the same as mine, except that my class inherits from object, but that's cause I'm running Python 2.6 and I thought that in 2.7 all classes automatically inherit from object. Try that, though, and see if it helps.
To make it even clearer: try just doing myClass.my_property = 4. Does that raise an exception? If not then it's an issue with inheriting from object - properties only work for new-style classes, i.e. classes that inherit from object.

How can I make a class in python support __getitem__, but not allow iteration?

I want to define a class that supports __getitem__, but does not allow iteration.
for example:
class B:
def __getitem__(self, k):
return k
cb = B()
for x in cb:
print x
What could I add to the class B to force the for x in cb: to fail?
I think a slightly better solution would be to raise a TypeError rather than a plain exception (this is what normally happens with a non-iterable class:
class A(object):
# show what happens with a non-iterable class with no __getitem__
pass
class B(object):
def __getitem__(self, k):
return k
def __iter__(self):
raise TypeError('%r object is not iterable'
% self.__class__.__name__)
Testing:
>>> iter(A())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'A' object is not iterable
>>> iter(B())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "iter.py", line 9, in __iter__
% self.__class__.__name__)
TypeError: 'B' object is not iterable
From the answers to this question, we can see that __iter__ will be called before __getitem__ if it exists, so simply define B as:
class B:
def __getitem__(self, k):
return k
def __iter__(self):
raise Exception("This class is not iterable")
Then:
cb = B()
for x in cb: # this will throw an exception when __iter__ is called.
print x

Categories

Resources