How to limit values in dataclass attribute? - python

I have the following dataclass Gear that I want to limit the maximum value for gear_level from 0 to 5. But as you can see when I increment gear_level, it goes higher than 5, which is not what I want. I tried method as well as postinit. How do I fix this problem?
from dataclasses import dataclass
#dataclass
class Gear:
gear_level: int = 0
direction: str = None
# more codes ...
def __postinit__(self):
if self.gear_level <= 0:
self.gear_level = 0
elif 5 > self.gear_level > 0:
self.gear_level = self.gear_level
else:
self.gear_level = 5
def set_gear_level(self, level):
if level <= 0:
self.gear_level = 0
elif 5 > level > 0:
self.gear_level = level
else:
self.gear_level = 5
g = Gear()
g.set_gear_level(6)
print(g)
g.gear_level += 1
print(g)
g.set_gear_level(-1)
print(g)
g.gear_level -= 1
print(g)
Ideally, I prefer to use the g.gear_level += 1 notation, because I want to increment gear_level. It should not jump from gear level 1 to 5. Also, when it decrement, it should stop at 0. It should take both an assignment of 0 and be allowed to decrement to 0. Can this be done?
Gear(gear_level=5, direction=None)
Gear(gear_level=6, direction=None)
Gear(gear_level=0, direction=None)
Gear(gear_level=-1, direction=None)

In this case I would simply use a property:
#dataclass
class Gear:
gear_level: int
# Rest of the class excluded for simplicity
#property
def gear_level(self) -> int:
return self._gear_level
#gear_level.setter
def gear_level(self, value: int) -> None:
self._gear_level = min(max(value, 0), 5)
This way you don't need to write a __post_init__ or have to remember to call specific methods: assignment to gear_level will be kept 0 <= gear_level <= 5, even with +=.

The suggested link in the comments provides an elegant solution for tackling this issue, e.g. using a custom descriptor class which should work with minimal changes needed on your end.
For example, here's how I'd define a BoundsValidator descriptor class to check that a class attribute is within an expected lower and upper bounds (note that either bounds are optional in this case):
from typing import Optional
try:
from typing import get_args
except ImportError: # Python 3.7
from typing_extensions import get_args
class BoundsValidator:
"""Descriptor to validate an attribute x remains within a specified bounds.
That is, checks the constraint `low <= x <= high` is satisfied. Note that
both low and high are optional. If none are provided, no bounds will be
applied.
"""
__slots__ = ('name',
'type',
'validator')
def __init__(self, min_val: Optional[int] = None,
max_val: Optional[int] = float('inf')):
if max_val is None: # only minimum
def validator(name, val):
if val < min_val:
raise ValueError(f"values for {name!r} have to be > {min_val}; got {val!r}")
elif min_val is None: # only maximum
def validator(name, val):
if val > max_val:
raise ValueError(f"values for {name!r} have to be < {max_val}; got {val!r}")
else: # both upper and lower bounds are given
def validator(name, val):
if not min_val <= val <= max_val:
raise ValueError(f"values for {name!r} have to be within the range "
f"[{min_val}, {max_val}]; got {val!r}")
self.validator = validator
def __set_name__(self, owner, name):
# save the attribute name on an initial run
self.name = name
# set the valid types based on the annotation for the attribute
# for example, `int` or `Union[int, float]`
tp = owner.__annotations__[name]
self.type = get_args(tp) or tp
def __get__(self, instance, owner):
if not instance:
return self
return instance.__dict__[self.name]
def __delete__(self, instance):
del instance.__dict__[self.name]
def __set__(self, instance, value):
# can be removed if you don't need the type validation
if not isinstance(value, self.type):
raise TypeError(f"{self.name!r} values must be of type {self.type!r}")
# validate that the value is within expected bounds
self.validator(self.name, value)
# finally, set the value on the instance
instance.__dict__[self.name] = value
Finally, here's the sample code I came up with to test that it's working as we'd expect:
from dataclasses import dataclass
from typing import Union
#dataclass
class Person:
age: int = BoundsValidator(1) # let's assume a person must at least be 1 years
num: Union[int, float] = BoundsValidator(-1, 1)
gear_level: int = BoundsValidator(0, 5)
def main():
p = Person(10, 0.7, 5)
print(p)
# should raise a ValueError now
try:
p.gear_level += 1
except ValueError as e:
print(e)
# and likewise here, for the lower bound
try:
p.gear_level -= 7
except ValueError as e:
print(e)
# all these should now raise an error
try:
_ = Person(0, 0, 2)
except ValueError as e:
print(e)
try:
_ = Person(120, -3.1, 2)
except ValueError as e:
print(e)
if __name__ == '__main__':
main()
This provides the output below when we run the code:
Person(age=10, num=0.7, gear_level=5)
values for 'gear_level' have to be within the range [0, 5]; got 6
values for 'gear_level' have to be within the range [0, 5]; got -2
values for 'age' have to be within the range [1, inf]; got 0
values for 'num' have to be within the range [-1, 1]; got -3.1

There is also an excellent library called param that enables you to achieve this rather easily. In your case it would look something like this:
import param
#dataclass
class Gear:
gear_level: int = param.Integer(1, bounds=(0,5))
direction: str = None
g = Gear()
print(g)
g.gear_level = 42 # This throws an exception
It has many other neat features for designing robust interfaces.

Related

How can I put limits on a variable type int in python?

I need to put a limit when entering a number for a certain attribute, how can I do that?
class Escuelas:
def __init__(self):
self.cod_provincia = 0:1
You should use Encapsulation principle.
class Escuelas:
def __init__(self):
self.__cod_provincia = 0
#property
def cod_provincia(self):
return self.__cod_provincia
#cod_provincia.setter
def cod_provincia(self, value):
if isinstance(value, int) and value in (0, 1):
self.__cod_provincia = value
else:
raise ValueError('Not Valid Code!')
if you want set value in specific range you just use range(start, end) instead of the tuple (0, 1) in setter method.
Maybe somewhere in your code, you need something like below:
if self.cod_provincia not in [0, 1]:
raise ValueError("Entrada de código no válida.")
If I understand you correctly, you want to force an int value to be within a certain range when it is set.
Is this what you require?
class Escuelas:
def __init__(self):
self.intValue = None
self.upperLimit = 100
def setIntValue(self,newValue):
self.intValue = self.upperLimit if newValue>self.upperLimit else newValue
e = Escuelas()
e.setIntValue(200)
print(e.intValue)
This sets the value to 100 (the upper limit) and not 200.

Pydantic - Validation Does not Happen

I am quite new to using Pydantic.
The Issue I am facing right now is that the Model Below is not raising the Expected Exception when the value is out of range.
For example, if you pass -1 into this model it should ideally raise an HTTPException. but nothing happens
I am not sure where I might be going wrong.
Any Advice would be great.
class GetInput:
"""
for the fields endpoint
"""
def __init__(self,
rank: Optional[int] = None,
interval: Optional[int] = None):
self.rank = rank
self.interval = interval
#validator('rank')
def check_if_rank_in_range(cls, v):
"""
check if input rank is within range
"""
if not 0 < v < 1000001:
raise HTTPException(
status_code=400, detail="Rank Value Must be within range (0,1000000)")
return v
#validator('interval')
def check_if_interval_in_range(cls, v):
"""
check if input rank is within range
"""
if not 0 < v < 1000001:
raise HTTPException(
status_code=400, detail="Interval Value Must be within range (0,1000000)")
return v
The FastAPI Endpoint
#router.get('fields/',status_code=200)
def get_data(params: GetInput = Depends()):
if params.rank:
result = get_info_by_rank(params.rank)
elif params.interval:
result = get_info_by_interval(params.interval)
return result
class GetInput(BaseModel):
rank: Optional[int]=None
interval: Optional[int]=None
#validator("*")
def check_range(cls, v):
if v:
if not 0 < v < 1000001:
raise HTTPException(status_code=400, detail="Value Must be within range (0,1000000)")
return v
the validator was not working due to not Inheriting the BaseModel Class
When the BaseModel Class would get inherited it would throw an error if either of the values is empty thus the additional if statement.

AssertEqual failing when comparing two seemingly identical IndexErrors

I have a DynamicArray class shown below. (I have only included relevant methods. The rest can be viewed from https://www.geeksforgeeks.org/implementation-of-dynamic-array-in-python/)
import ctypes
class DynamicArray:
'''
Dynamic Array class
'''
def __init__(self):
self.n = 0 # count number of elements
self.capacity = 1 # default capacity
self.A = self.make_array(self.capacity)
def __len__(self):
"""
Return number of elements in the array
"""
return self.n
def __getitem__(self,k):
"""
Return element at k index
"""
#Check if K index is out of bounds#
if not 0 <= k < self.n:
return IndexError('{} is out of bounds'.format(k))
return self.A[k] #Retrieve from the array at index k#
Then I have another unit test file down below
from DynamicArray import DynamicArray
import unittest
class MyTestCase(unittest.TestCase):
def setUp(self) -> None:
self.a = DynamicArray() # empty array
self.b = DynamicArray()
self.b.append(0)
self.c = DynamicArray()
self.c.append(0)
self.c.append(1)
def test_getitem(self):
self.assertEqual(self.a.__getitem__(0),IndexError('0 is out of bounds'))
When I run the test I expect self.a.__getitem__(0) to throw IndexError('0 is out of bounds') and I can't see why the assertion fails? The only difference is that self.a.__getitem__(0) will yield IndexError('{} is out of bounds'.format(0)), which seems to me the same as IndexError('0 is out of bounds')
I tried running below code to see if the string by themselves were any different
if '{} is out of bounds'.format(0) == '0 is out of bounds':
print('str equal')
if '{} is out of bounds'.format(0).__len__() == '0 is out of bounds'.__len__():
print('len equal')
if IndexError('{} is out of bounds'.format(0)) == IndexError('0 is out of bounds'):
print('IndexError equal')
and confirmed that only the third if statement did not print
below is the photo of the console
Thanks in advance. Constructive criticisms and feedbacks are welcome.
Exceptions can't be conpared with assertEqual.
with self.assertRaises(IndexError, msg='0 is out of bounds'):
self.a[0]
And Exceptions must be raiseed to be captured.
You're returning IndexError
raise IndexError('{} is out of bounds'.format(k))
https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises

Get setter status in python

I am looking to return a status when setting an instance variable. This return value will either indicate that the value was set or the value was invalid. For example:
class C():
def __init__(self):
self._attr = 1
#property
def attr(self):
return self._attr
#attr.setter
def attr(self, val):
if val < 10:
self._attr = val
return 'value set'
else:
return 'value must be < 10'
status = C().attr = 11 # should return failed
status = C().attr = 9 # should return worked
Adding return values to all my setters would be time-consuming and seems to be bad practice. Is there a better way to get a status from a setter?
The other solution I thought of was to write a "setSetter()" function that calls attr = val then checks if attr == val (which works like a status variable). However, it seems like there should be a better method.
Also, if there is a better way to structure the flow control so that attributes aren't set to invalid values I'm open to changing my strategy.
Thanks in advance for the help!
The standard way to indicate failure in Python and/or prevent code from reaching an invalid state is to raise an exception to indicate you've encountered a situation where you can't continue execution. This doesn't require you to modify the return type:
#attr.setter
def attr(self, val: int) -> None:
if val < 10:
self._attr = val
else:
raise ValueError('value must be < 10')
try:
C().attr = 9 # works
C().attr = 11 # raises ValueError and goes to nearest matching except
C().attr = 13 # is never executed
except ValueError as e:
print(e) # prints 'value must be < 10'

How can I elegantly find the next and previous value in a Python Enum? [duplicate]

This question already has answers here:
Get next enumerator constant/property
(4 answers)
Closed 6 years ago.
I have a simple enum in Python that looks like this:
from enum import Enum
class MyEnum(Enum):
#All members have increasing non-consecutive integer values.
A = 0
B = 2
C = 10
D = 18
...
I want functions pred() and succ() that given a member of MyEnum return the member of MyEnum that precedes and succeeds the given element, respectively (just like the functions of the same name in Haskell). For example succ(MyEnum.B) and pred(MyEnum.D) should both return MyEnum.C. An exception can be raised if succ is called on the last member of pred is called on the first member.
There doesn't seem to be any built in method to do this, and while I can call iter(MyEnum) to iterate over the values it has to go through the whole enum from the beginning. I could probably implement a sloppy loop to accomplish this on my own, but I know there are some real Python gurus on this site, so to you I ask: is there a better approach?
Note that you can provide succ and pred methods inside an Enum class:
class Sequential(Enum):
A = 1
B = 2
C = 4
D = 8
E = 16
def succ(self):
v = self.value * 2
if v > 16:
raise ValueError('Enumeration ended')
return Sequential(v)
def pred(self):
v = self.value // 2
if v == 0:
raise ValueError('Enumeration ended')
return Sequential(v)
Used as:
>>> import myenums
>>> myenums.Sequential.B.succ()
<Sequential.C: 4>
>>> myenums.Sequential.B.succ().succ()
<Sequential.D: 8>
>>> myenums.Sequential.B.succ().succ().pred()
<Sequential.C: 4>
Obviously this is efficient only if you actually have a simple way to compute the values from an item to the next or preceding one, which may not always be the case.
If you want a general efficient solution at the cost of adding some space, you can build the mappings of the successor and predecessor functions.
You have to add these as attributes after the creation of the class (since Enum messes up attributes) so you can use a decorator to do that:
def add_succ_and_pred_maps(cls):
succ_map = {}
pred_map = {}
cur = None
nxt = None
for val in cls.__members__.values():
if cur is None:
cur = val
elif nxt is None:
nxt = val
if cur is not None and nxt is not None:
succ_map[cur] = nxt
pred_map[nxt] = cur
cur = nxt
nxt = None
cls._succ_map = succ_map
cls._pred_map = pred_map
def succ(self):
return self._succ_map[self]
def pred(self):
return self._pred_map[self]
cls.succ = succ
cls.pred = pred
return cls
#add_succ_and_pred_maps
class MyEnum(Enum):
A = 0
B = 2
C = 8
D = 18
Used as:
>>> myenums.MyEnum.A.succ()
<MyEnum.B: 2>
>>> myenums.MyEnum.B.succ()
<MyEnum.C: 8>
>>> myenums.MyEnum.B.succ().pred()
<MyEnum.B: 2>
>>> myenums.MyEnum._succ_map
{<MyEnum.A: 0>: <MyEnum.B: 2>, <MyEnum.C: 8>: <MyEnum.D: 18>, <MyEnum.B: 2>: <MyEnum.C: 8>}
you probably want a custom exception instead of KeyError but you get the idea.
There probably is a way to integrate the last step using metaclasses, but it's notstraightforward for the simple fact thatEnums are implemented using metaclasses and it's not trivial to compose metaclasses.
Adding your next and prev methods (or succ and pred) is easy enough:
def next(self):
cls = self.__class__
members = list(cls)
index = members.index(self) + 1
if index >= len(members):
# to cycle around
# index = 0
#
# to error out
raise StopIteration('end of enumeration reached')
return members[index]
def prev(self):
cls = self.__class__
members = list(cls)
index = members.index(self) - 1
if index < 0:
# to cycle around
# index = len(members) - 1
#
# to error out
raise StopIteration('beginning of enumeration reached')
return members[index]

Categories

Resources