# -*- coding: utf-8 -*-
"""
This code is taken from the itertree package:
https://pypi.org/project/itertree/
GIT Home:
https://github.com/BR1py/itertree
The documentation can be found here:
https://itertree.readthedocs.io/en/latest/index.html
The code is published under MIT license incl. human protect patch:
The MIT License (MIT) incl. human protect patch
Copyright © 2022 <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the “Software”), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Human protect patch:
The program and its derivative work will neither be modified or executed to harm any human being nor through
inaction permit any human being to be harmed.
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information see: https://en.wikipedia.org/wiki/MIT_License
This part of code contains the helper functions related to the `iTree` data attribute
"""
import copy
import itertools
import abc
import fnmatch
import re
from collections import deque, namedtuple
from itertree.itree_helpers import *
from itertree.itree_filters import *
from itertree.itree_mathsets import *
# special internal constant used for the item that is stored without giving a key
[docs]class iTValueModel(abc.ABC):
"""
This is the replacement for the old `iTDataModel()`-class.
This class can be used to define data models for the values that might be placed in the `iTree`.
The model should define min more detail which value objects are accepted or not and it defines also how not
matching objects are handled:
* Deny the value and raise a ValueError exception
* Cast the value into a valid value object
But the definition of the data model allows limitations which goes far away from just data-type related topics.
E.g. In the model the user can limit numerical values to a specific range (interval) or he might
limit strings to specific characters.
All definitions related to checks and type casts must be defined by the user by overwriting the method
`check_and_cast_single_item(value_item)` . The return of the method must e the checked and cast value. if the value
does not match the method should raise a `ValueError` .
The method should always check and cast a single item, this is important. In case a list or an array like value
should be stored in the model the base model will manage the required iteration over the sub-items and perform
and utilize the single item check via the user defined method.
.. note:: In case a value like `[1,2,3,45]` is given each item in the list will be checked and/or casted.
This leads us to an important second definition functionality related to the model related to the size of
dimensions (shape) of the value stored in the model. The definition
of the shape is given as a parameter when the object is instanced. For the shape we expect a tuple with
the dimension information. If a value object is given the maximum shape will be calculated and this will be
compared with the expected one. The maximum is used because in nested lists the user can define
sub-list with different length. Strings or bytes aare also seen as arrays in this case!
We have the following possibilities to define shapes:
* shape=Any -> accept any shape of the given value (no check performed)
* shape=tuple() -> empty tuple given no dimension expected model will accept single values only!
* shape=(Any,) -> tuple containing one element which is the Any helper class; We will accept single values or
any 1 dimensional object here (e.g. values like: `1`; `'abc'`; `[1,2,3,4]` )
* shape=(10,) -> tuple containing one element. We expect one dimensional values with a length lower
or equal to the given integer number
* shape=(INF,) -> tuple containing one element that is INF (infinite). We expect one dimensional values
of any length
* shape=(3,4) -> Two dimensions expected with fixed size (e.g. [[1],[2,3]] would match)
* shape=(INF,4) -> Two dimensions expected with first length unlimited and second length limited to 3
* shape=(4,ANY) -> Minimum one dimensions expected with first length limited to 4; here the user
can also put infinite dimensions in (e.g [1]; [[1],[2]] ; [[[[888],[202,500]]]] would fit)
* shape=(4,ANY,INF) -> 1 dimension or 3 dimensions accepted, 2 dimension will not be accepted
.. note:: The model base object `iTValueModel()` contains two checking levels. First the user defined
check via method definition for checks and casts of single items given. In second step the model also
checks the dimension (shape) of the given value.
In case a `str` or `bytes` objects are given the behavior related to the checks will be a bit different
as for the other objects. The method `check_and_cast_single_item(value_item)` will target the whole string
as a single item! But the shape check will be done also on the string as an object with a length.
This means a string is a 1 dimensional object and the user might limit the size of the string via a shape.
(E.g.: The object "Hello" has the shape: (5,); the object ['one', 'two','three'] has the shape: (3,5) )
The user might use the method 'get_max_shape()' to measure the shape of objects that is considered
in the model base class.
During the instance of the object a formatter can be defined too. This might help the user e.g. do define if an
integer value should be converted to a hex or binary representation during string conversion. The build-in command
str() of this model class will deliver the formatted value only. The repr() will deliver the class definition.
To use the model the user should put the instanced model object as value in the `iTree`. The real value objects
can be placed during object instance via the parameter value or later on via the `set()` method of the model
(value exchange too). In case the value is not matching to the model definition an ValueError exception
will be raised.
If the user like to test first if the value is matching he can use the `in` keyword to check this. In case of no
match the exception content might be picked via the last_exception property of the model in this case (might give
a hint why the value is not accepted).
Standard Parameters:
:param value: value object to be stored in the model (must match to the model). In case no value is stored
in the model (empty model) the value will be NoValue.
:param description: Description string
:param shape: Define the dimensions the object should have:
* None - shape is ignored object might have dimensions or not
* tuple() - empty tuple or iterable - value object will have no size/dimension
* (InfShape) - one dimensional value object with infinite size
* (100) - one dimensional value object with max size of 100 items
* (100,100) - two dimensional object with max size of 100 in each dimension
* (InfShape,InfShape,InfShape) - three-dimensional object with infinite size in each dimension
* ...
.. note:: For multi-dimensional objects it's recommended to use numpy arrays or objects which have
the attribute `shape` representing the size for each dimension available
instead of tuples or lists. If not the object
performance might be worse (internal iterations required to measure the shape).
:param formatter: Formatter for the single item of the value object (see string formatting in python)
In case no formatter is given `str()` will be used for creation of the
item string representation.
"""
__slots__ = ('_value', '_description', '_shape', '_formatter', '_last_exception','_contains')
def __init__(self, value=NoValue, description=None, shape=Any, contains=None,formatter=str):
"""
< Base class for Value Type Model definitions to limit the usable objects that might be placed as values in the
iTree objects (might be used for other proposes too).
:type value: object
:param value: value object to be stored in the model (must match to the model). In case no value is stored
in the model (empty model) the value will be NoValue.
:type description: str
:param description: Description string
:type shape: Union[tuple,None]
:param shape: Define the dimensions the object should have:
* None - shape is ignored object might have dimensions or not
* tuple() - empty tuple or iterable - value object will have no size/dimension
* (INFe,) - one dimensional value object with infinite size
* (100,) - one dimensional value object with max size of 100 items
* (100,100) - two dimensional object with max size of 100 in each dimension
* (INF,INF,INF) - three-dimensional object with infinite size in each dimension
* (Any) - single value or one dimensional object
* ...
..note:
For multi-dimensional objects it's recommended to use numpy arrays or objects which have
the attribute `shape` representing the size for each dimension available
instead of tuples or lists. If not the object
performance might be worse (internal iterations required to measure the shape).
:type contains: Union[iTSet,iTFilter,object,None]
:param contains: This filter definition uses `__contains__()` to check if the given value is matching
If `None` is given filtering is inactive.
..warning:: The filtering in `iTValueModel`-class differs from filtering we
have in `iTree`-class which uses filters compatible with build-in `filter()`method..
Here the containment is checked via `__contains__().
The `itertree` objects under `Filters` do all have both checks available and
can be used therefore in both cases.
:param formatter: Formatter for the single item of the value object (see string formatting in python)
In case no formatter is given str() will be used for creation of the
item string representation.
"""
if description:
self._description = str(description)
else:
self._description = None
if shape is not Any:
shape = tuple(shape)
for i in shape:
t = type(i)
if t is not int and i is not Any and i != INF:
raise ValueError('Parameter shape must be an iterable of integers or InfShape objects')
self._shape = shape
if formatter is None:
formatter=str
if callable(formatter):
self._formatter = formatter
else:
self._formatter = str(formatter)
self._contains=contains
if value is NoValue:
self._value=NoValue
else:
self._value = self.__contains__(value,True)
self._last_exception = None
def __eq__(self, other):
if type(other)!=type(self):
return False
return self.get_init_args()==other.get_init_args()
[docs] def check_and_cast_single_item(self, value_item):
"""
method that should be overwritten in the user models
Depending on the requirements the input value might be casted in a target type and he can be checked before
or afterwards against check criteria for matches. In case of no match a
ValueError should be raised
:except: Raise ValueError in case given value does not match
:param value_item: The value given to the model
:return: casted and checked value
"""
return value_item
def _recursive_check(self, value_iterable,raise_exception=False):
"""
helper generator function used for iterable objects match checks
:param value_iterable: iterable value
:param raise_exception: flag that will raise an exception if not set the not matching elements
are not yielded back (ignored)
"""
if self._contains is not None:
contains=self._contains
for v in value_iterable:
if hasattr(v, '__iter__') or hasattr(v, '__next__'):
if hasattr(v, 'capitalize'): # str or byte
v=self.check_and_cast_single_item(v)
if v in contains:
yield v
else:
if raise_exception:
raise ValueError('Given sub value does not match to given filter_method (out of range)')
else:
yield list(self._recursive_check(v,raise_exception))
else:
v=self.check_and_cast_single_item(v)
if v in contains:
yield v
else:
if raise_exception:
raise ValueError('Given sub value does not match to given filter_method (out of range)')
else:
for v in value_iterable:
if hasattr(v, '__iter__') or hasattr(v, '__next__'):
if hasattr(v, 'capitalize'): # str or byte
yield self.check_and_cast_single_item(v)
else:
yield list(self._recursive_check(v))
else:
yield self.check_and_cast_single_item(v)
def _check_shape(self, value_shape):
"""
helper function checking if the shape of the given value matches to the expected shape of the model
:except: In case the shape does not match a ValueError will be raised
:param value_shape: shape of the value
:return: True
"""
target_shape = self._shape
if target_shape is Any:
return True
target_shape_size = len(target_shape)
i = -1
for i, v_s in enumerate(value_shape):
if i > target_shape_size - 1:
if len(target_shape) != 0 and target_shape[-1] is Any:
return True
else:
raise ValueError('Given value shape=%s has more dimensions as model accepts '
'(model-shape=%s)' % (repr(value_shape), self.__repr_shape(target_shape)))
t_s = target_shape[i]
if not (t_s is Any or t_s == INF) and v_s > t_s:
raise ValueError('Given value shape=%s (position=%i) too large for model '
'(shape=%s)' % (repr(value_shape), i, self.__repr_shape(target_shape)))
if i == -1 and len(target_shape) != 0 and target_shape[0] is not Any:
raise ValueError('Given value shape=%s too small for model (shape=%s) ->expecting more dimensions'
% (repr(value_shape), self.__repr_shape(target_shape)))
if i < target_shape_size - 1:
if target_shape[i + 1] is not Any:
raise ValueError('Given value shape=%s too small for model (shape=%s) -> expecting more dimensions'
% (repr(value_shape), self.__repr_shape(target_shape)))
return True
def _get_max_shape(self, value):
"""
helper method that calculates the shape of the given value in case we have multiple dimensions the
method must iterate over all items which might be slow
:param value: value the shape should be calculated for
:return:
"""
if hasattr(value, '__len__'):
s0 = len(value)
if s0 == 0:
return ()
if s0 == 1 and hasattr(value, 'capitalize'): # handle string like objects
return ()
if hasattr(value, 'capitalize'):
return (len(value),)
s1 = 0
sub_shape = []
# via zip_longest we iterate over the longest sub_list item
for v2 in itertools.zip_longest(*(v if hasattr(v, '__len__') else tuple() for v in value), fillvalue=0):
s1 += 1 # calc the length of second dimension
for v3 in v2:
sub_shape_new = self._get_max_shape(v3) # recursive measurement of lower dimensions
# compare new shapes with old shapes (find max)
for i, new in enumerate(sub_shape_new):
if i >= len(sub_shape):
# new items
sub_shape.extend(sub_shape_new[i:])
else:
# is max?
if sub_shape[i] < new:
sub_shape[i] = new
if len(sub_shape):
return (s0, s1) + tuple(sub_shape)
elif s1:
return (s0, s1)
else:
return (s0,)
else:
return ()
def __contains__(self, value, _return_value=False):
"""
Main checking function if a value matches in the model
:param value: value to be checked
:param _return_value: internal boolean parameter if we should give back the value itself
:return: True/False or value
"""
if value is NoValue: # same as clear()
if _return_value:
return NoValue
return True
try:
if hasattr(value, '__iter__') or hasattr(value, '__next__'):
if hasattr(value, 'capitalize'): # str or byte
value = self.check_and_cast_single_item(value)
if self._contains is not None:
if value not in self._contains:
raise ValueError('Given value does not match to given filter_method (out of range)')
else:
value = list(self._recursive_check(value,True))
else:
value = self.check_and_cast_single_item(value)
if self._contains is not None:
if value not in self._contains:
raise ValueError('Given value does not match to given filter_method (out of range)')
if self._shape is not Any:
self._check_shape(self._get_max_shape(value))
self._last_exception = None
if _return_value:
return value
else:
return True
except Exception as e:
self._last_exception = e
if _return_value:
return self._value
else:
return False
[docs] def set(self, value):
"""
Set the value of the model in case the value does not match a ValueError exception will be raised
:param value: value to be placed inside the model
:return: old value stored in the model
"""
value = self.__contains__(value, True)
if self._last_exception:
raise self._last_exception
old, self._value = self._value, value
return old
[docs] def get(self):
"""
get the value that is placed inside the model
If no value is stored in the model the `NoValue`-object will be given back
:return: value stored in the model
"""
return self._value
@property
def value(self):
"""
property delivering the value stored in the model
:return: value stored in the model
"""
return self._value
@property
def description(self):
"""
optional description of the model
:return: description related to the model
"""
return self._description
[docs] def set_description(self, description):
"""
set/exchange the description of the model
:param description:
:return: old description
"""
old, self._description = self._description, str(description)
return old
@property
def formatter(self):
"""
get the formatter stored in the model
:return: formatter object
"""
return self._formatter
@property
def contains(self):
"""
contains object stored in the model
:return:
"""
return self._contains
[docs] def clear(self):
"""
deletes teh value store din the model and place the `NoValue`-object in
"""
self._value = NoValue
@property
def last_except(self):
"""
get the last exception
:return: last exception raised by the model related the storage or check of a value
"""
return self._last_exception
@property
def is_iTValueModel(self):
"""
used for model identification
:return: True
"""
return True
[docs] def get_init_args(self, full=False,clear=False):
"""
deliver all initial arguments used to instance this model object
:param full: True give always full list
False (default) list is shortened in case of default parameter values
:param clear: True - use NoValue object as value (ignore stored value)
False - stored value is included in parameter tuple
:return: Tuple of initial parameters
"""
if clear:
value=NoValue
else:
value=self._value
if full or self._formatter is not str:
return (value, self._description, self._shape, self._contains,self._formatter)
elif self._contains is not None:
return (value, self._description, self._shape, self._contains)
elif self._shape is not Any:
return (value, self._description, self._shape)
elif self._description is not None and self._description!='':
return (value, self._description)
else:
return (value,)
def __repr_shape(self, shape):
"""
helper function for the repr of the shape
:param shape: shape to be converted into a string representation
:return: string representation of given shape
"""
if shape is Any:
return 'Any'
out = ['(']
for i in self._shape:
if i is Any:
out.append('Any')
out.append(',')
elif i == INF:
out.append('INF')
out.append(',')
else:
out.append(repr(i))
out.append(',')
out[-1] = ')'
return ''.join(out)
def __repr__(self):
"""
object representation
:return: representation string
"""
out = ['%s(' % self.__class__.__name__]
#out.append(self.__str__())
for item in self.get_init_args():
if item is Any:
out.append('Any')
else:
out.append(repr(item))
out.append(', ')
out[-1] = ')'
return ''.join(out)
def __create_item_str(self,value):
"""
helper method for formatting of the given value
:param value: value to be formatted
:return: string of the formatted value
"""
if hasattr(value, '__iter__') or hasattr(value, '__next__'):
if hasattr(value, 'capitalize'): # str or byte
if callable(self._formatter):
return self._formatter(value)
else:
try:
return self._formatter.format(value)
except:
return self._formatter%value
else:
out='['+''.join(self.__create_item_str(v)+', ' for v in value)
return out[:-2]+']'
else:
if callable(self._formatter):
return self._formatter(value)
else:
try:
return self._formatter.format(value)
except:
return self._formatter % value
def __str__(self):
"""
string representation of the model value
here we give just the representation of the stored value not the model object back
:return: item string
"""
if self._value == NoValue:
return 'NoValue'
return self.__create_item_str(self._value)
[docs]class iTAnyValueModel(iTValueModel):
"""
Model that will take any python object without any restrictions
"""
[docs] def check_and_cast_single_item(self, value_item):
"""
required overload will allow any object to be stored in the model
:param value_item: potential value to be stored in the model
:return: confirmed value to be stored in the model
"""
return value_item
[docs]class iTRoundIntModel(iTValueModel):
"""
Model that would store integer values
The model accepts any object that can be casted into a float and rounded to an integer
to be stored as a int in the model
"""
[docs] def check_and_cast_single_item(self, value_item):
return round(float(value_item))
[docs]class iTIntModel(iTValueModel):
"""
This integer model allows only integers or strings containing a decimal integer to be stored in the model
as int value
"""
[docs] def check_and_cast_single_item(self, value_item):
if type(value_item) in {int, str}:
return int(value_item)
raise ValueError('Given value type is not matching to data model')
[docs]class iTInt8Model(iTValueModel):
"""
Integer model that limits the given values to int8 values
"""
interval = mSetInterval(-128, 127)
def __init__(self,value=NoValue,description=None,shape=Any,contains=None,formatter=str):
if contains:
contains=mSetCombine(contains,self.interval,is_union=False)
else:
contains=self.interval
super().__init__(value,description,shape,contains=contains,formatter=formatter)
[docs] def check_and_cast_single_item(self, value_item):
if type(value_item) not in {int, str}:
raise ValueError('Given value type is not matching to data model')
return int(value_item)
[docs] def get_init_args(self, full=False,clear=False):
if clear:
value = NoValue
else:
value = self._value
if full or self._formatter is not str:
return super().get_init_args(full, clear)
elif self._contains is not None and self._contains!=self.interval:
return (value, self._description, self._shape, self._contains)
elif self._shape is not Any:
return (value, self._description, self._shape)
elif self._description is not None and self._description != '':
return (value, self._description)
else:
return (value,)
[docs]class iTUInt8Model(iTInt8Model):
"""
Integer model that limits the given values to uint8 values
"""
interval = mSetInterval(0,255)
[docs]class iTInt16Model(iTInt8Model):
"""
Integer model that limits the given values to int16 values
"""
interval = mSetInterval(-32768, 32767)
[docs]class iTUInt16Model(iTInt8Model):
"""
Integer model that limits the given values to uint16 values
"""
interval = mSetInterval(0, 65535)
[docs]class iTInt32Model(iTInt8Model):
"""
Integer model that limits the given values to int32 values
"""
interval = mSetInterval(-2147483648, 2147483647)
[docs]class iTUInt32Model(iTInt8Model):
"""
Integer model that limits the given values to uint32 values
"""
interval = mSetInterval(0, 2 ** 32 - 1)
[docs]class iTInt64Model(iTInt8Model):
"""
Integer model that limits the given values to int64 values
"""
interval = mSetInterval(-9223372036854775808, 9223372036854775807)
[docs]class iTUInt64Model(iTInt8Model):
"""
Integer model that limits the given values to uint64 values
"""
interval = mSetInterval(0, 18446744073709551615)
[docs]class iTFloatModel(iTValueModel):
"""
Float model that allows any float or string that can be casted to float to be stored in the model as float value
"""
[docs] def check_and_cast_single_item(self, value_item):
return float(value_item)
[docs]class iTStrModel(iTValueModel):
"""
A model to store a string
"""
[docs] def check_and_cast_single_item(self, value_item):
return str(value_item)
[docs]class iTStrFnPatternModel(iTStrModel):
"""
A model to store a string that matches to the fnmatch pattern
"""
def __init__(self,value=NoValue,description=None,shape=Any,contains=None,pattern=None,formatter=None):
if pattern is not None:
if contains:
contains=iTFilterIntersection(contains,iTFilter(fnmatch.fnmatch,pattern))
else:
contains=iTFilter(fnmatch.fnmatch,pattern)
self._pattern=pattern
super().__init__(value,description,shape,contains=contains,formatter=formatter)
@property
def pattern(self):
return self._pattern
[docs] def get_init_args(self, full=False,clear=False):
if full or self._formatter is not str:
args = super().get_init_args(full=True, clear=clear)
return args[:-1]+(self._pattern,args[-1])
elif self._pattern is not None:
args = super().get_init_args(full=True, clear=clear)
return args[:-1] + (self._pattern,)
else:
return super().get_init_args(full=full, clear=clear)
[docs]class iTStrRegexPatternModel(iTStrModel):
"""
A string model that matches to the regex pattern
"""
def __init__(self, value=NoValue, description=None, shape=Any, contains=None, pattern=None, formatter=None):
if pattern is not None:
if contains:
contains = iTFilterIntersection(contains, iTFilter(lambda i: re.fullmatch(pattern,i) is not None))
else:
contains = iTFilter(lambda i: re.fullmatch(pattern,i) is not None)
self._pattern = pattern
super().__init__(value, description, shape, contains=contains, formatter=formatter)
@property
def pattern(self):
return self._pattern
[docs] def get_init_args(self, full=False, clear=False):
if full or self._formatter is not str:
args = super().get_init_args(full=True, clear=clear)
return args[:-1] + (self._pattern, args[-1])
elif self._pattern is not None:
args = super().get_init_args(full=True, clear=clear)
return args[:-1] + (self._pattern,)
else:
return super().get_init_args(full=full, clear=clear)
[docs]class iTASCIIStrModel(iTValueModel):
"""
A string model that accepts only ASCII characters
"""
[docs] def check_and_cast_single_item(self, value_item):
value = str(value_item)
for i, c in enumerate(value):
if ord(c) >= 128:
raise ValueError('Non ASCII character %s found in value (position=%i) '
'-> not accepted by model' % (repr(c), i))
return value
[docs]class iTUTF8StrModel(iTValueModel):
"""
A string model that accepts only UTF-8 characters
"""
[docs] def check_and_cast_single_item(self, value_item):
value = str(value_item)
try:
value.encode('unicode-escape').decode('UTF-8')
except:
raise ValueError('Non UTF-8 character found in given value '
'-> not accepted by model')
return value
[docs]class iTUTF16StrModel(iTStrModel):
"""
A string model that accepts only UTF16 characters
"""
[docs] def check_and_cast_single_item(self, value_item):
value = str(value_item)
try:
value.encode('UTF-16').decode('UTF-16')
except:
raise ValueError('Non UTF-16 character found in given value '
'-> not accepted by model')
return value
[docs]class iTEnumerateModel(iTValueModel):
def __init__(self,value=NoValue,enumerate_dict={}):
if NoValue not in enumerate_dict:
enumerate_dict[NoValue]=NoValue
self.cast_dict={v:k for k,v in enumerate_dict.items()}
for k in enumerate_dict.keys():
if k is not NoValue and k!=int(k):
raise TypeError('Given enumerate_dict must have integer keys only!')
self.cast_dict[k]=k
self.enumerate_dict=enumerate_dict
super().__init__(value,formatter=self.enumerate_dict.get)
[docs] def check_and_cast_single_item(self, value_item):
if value_item is NoValue:
return NoValue
try:
value=self.cast_dict[value_item]
except KeyError:
raise ValueError('Given value does not match to enumeration definition of the model')
return value
# ************************************************************************************************
# ******* The rest of the definitions are kept from older versions just to be downward compatible!
# Please do not use for new projects the classes are outdated
# ************************************************************************************************
__NOVALUE__=NoValue
__NOKEY__=NoKey
# return_types
VALUE = V = 0 # returns the stored value
STR = S = 1 # returns the string representation of the value (DTDataItems contains formatters for this)
FULL = F = 2 # In case DTDataItem objects are used for storage the full object is given back
[docs]class iTDataValueError(ValueError):
"""
Exception to be raised in case a validator finds a non matching value related to the iDataModel
"""
pass
[docs]class iTDataTypeError(ValueError):
"""
Exception to be raised in case a validator finds a non matching value type related to the iDataModel
"""
pass
[docs]class iTDataModel(abc.ABC):
"""
The default iTree data model class
This the interface definition for specific data model classes
that might be created using this superclass
The data model checks the given value for a specific data item.
So that we can ensure that the given value matches to the expectations.
We can check for types, shapes (length), limits, or matching patterns.
Besides the check we can also define a default formatter for the value that is used when
it is translated into a string.
(see examples/itree_data_examples.py)
"""
__slots__ = ('_value', '_formatter_cache')
def __init__(self, value=NoValue):
"""
:param value: value object to be stored in the data model
"""
if not value == NoValue:
value = self.validator(value)
self._value = value
self._formatter_cache = None
def __contains__(self, item):
"""
:param item: item to be checked if it is equal to the stored value
:return: True/False
"""
return self._value == item
def __format__(self, format_spec=None):
"""
If no format spec is given we format with the predefined internal formatter
:param format_spec: None or format specification for the value
:return: formatted string
"""
if self.is_empty:
# we might create an exception here when we have numerical values!
# must be overloaded!
return 'None'
if format_spec is None or format_spec == '':
# as long as the value is not changed we cache the result for quicker reuse:
if self._formatter_cache is None:
# run the formatter
self._formatter_cache = self.formatter(self.value)
return self._formatter_cache
else:
return self.value.__format__(format_spec)
def __repr__(self):
if self.is_empty:
return '%s()' % self.__class__.__name__
return '%s(value= %s)' % (self.__class__.__name__, self._value)
def __eq__(self, other):
if isinstance(other, iTDataModel):
return self.value == other.value
return self.value == other
def __ne__(self, other):
return not self.__eq__(other)
@property
def is_empty(self):
"""
tells if the iTreeDataModel is empty or contains a value
:return:
"""
return self._value == NoValue
@property
def is_iTDataModel(self):
return True
[docs] def get(self):
"""
the stored value
:return: object stored in value
"""
return self._value
[docs] def set(self, value):
"""
put a specific value into the data model
:except: raises an iTreeValidationError in case a not matching object is given
:param value: value object to be placed in the data model
"""
if value is NoValue:
old, self._value = self._value, NoValue
else:
old, self._value = self._value, self.validator(value)
self._formatter_cache = None
return old
value = property(get, set)
[docs] def check(self, value):
"""
put a specific value into the data model
:except: raises an iTreeValidationError in case a not matching object is given
:param value: value object to be placed in the data model
"""
try:
self._value = self.validator(value)
return True, ''
except Exception as e:
return False, str(e)
[docs] def clear(self, ):
"""
clears (deletes) the current value content and sets the state to "empty"
:return: returns the value object that was stored in the iTreeDataModel
"""
old, self._value = self._value, NoValue
self._formatter_cache = None
return old
[docs] @abc.abstractmethod
def validator(self, value):
"""
This method should check the given value.
It should raise an iDataValueError Exception with a failure explanation in case the value is not
matching to the iDataModel.
..warning:: The validator in an explicit iDataModel class must always return the value itself and it must raise
the iDataValueError in case of a no matching value. It should also call the super().validator()
method or at least consider that `NoValue` is a no matching value.
:except: iDataValueError in case value is not matching
:param value: to be checked against the model
:return: value (which might be casted)
"""
# we actually accept here any value
return value
[docs] @abc.abstractmethod
def get_init_args(self):
return (self._value,)
[docs]class iTDataModelAny(iTDataModel):
"""
Example iDataModel class that accepts any kind of value
"""
# we must overload the following mandatory abstract methods:
[docs] def validator(self, value):
return super().validator(value)
[docs] def get_init_args(self):
return tuple()
[docs]class iTData(dict):
"""
Standard itertree Data management object might be overloaded or changed by the user
"""
GET_LOOK_UP_METHOD = {STR: lambda item: format(item) if isinstance(item, iTDataModel) else str(item),
FULL: lambda item: item,
VALUE: lambda item: item.value if isinstance(item, iTDataModel) else item
}
def __init__(self, seq=None, **kwargs):
"""
Standard iTreeData object might be overloaded or changed by the user.
Stores the data in a internal dict. For attribute like data it's recommended to store
the data as iTreeDataItem. This object allows the definition of data type, sizes, limits and format definition
of a string representation.
:param data_items: single object or dict with key,value objects to be stored in the iTreeData object
"""
if not kwargs:
if seq is None:
super().__init__()
else:
try:
super().__init__(seq)
except:
super().__init__([(__NOKEY__, seq)])
else:
if seq is None:
super().__init__(**kwargs)
else:
try:
super().__init__(seq, **kwargs)
except TypeError:
super().__init__([(__NOKEY__, seq)], **kwargs)
def __setitem__(self, *args,key=__NOKEY__, value=__NOVALUE__):
"""
setter for the iTreeData object
HINT: If no value is given the key item will be interpreted as value
and it will be stored as __NOKEY__-object.
:param key: key under which the given object is stored
:param value: object that should be stored
:return: None
"""
l = len(args)
if key==__NOKEY__:
if value==__NOVALUE__:
if l==1:
value=args[0]
else:
try:
key,value=args
except:
raise AttributeError('Wrong number of positional arguments')
else:
if l==1:
key = args[0]
elif l!=0:
raise AttributeError('Wrong arguments given')
else:
if l!=0:
raise AttributeError('Wrong number of positional arguments')
try:
return super().__getitem__(key).set(value, _it_data_model_identifier=0)
except (KeyError, AttributeError, TypeError):
if key is __NOKEY__ and value is __NOVALUE__:
if super().__contains__(key):
super().__delitem__(key)
else:
return super().__setitem__(key, value)
def __getitem__(self, key=__NOKEY__, _return_type=VALUE):
"""
get a specific data item by key
:except: Will raise KeyError in case given key is unknown
:param key: key of the data item (if not given __NOKEY__ is used!
:param _return_type: We can deliver different returns
* VALUE - value object
* FULL - iTreeDataModel (only if used else same as VALUE)
* STR - formatted string representation of the data value
..note :: The parameter is only used by the helper method `get()`
and cannot be used by standard item access
:return: requested value
"""
item = super(iTData, self).__getitem__(key)
return self.GET_LOOK_UP_METHOD[_return_type](item)
def __delitem__(self, key=__NOKEY__, _value_only=True):
"""
delete a item by key
:except: KeyError is raised in case item key is unknown
:param key: key of the data item (if not given __NOKEY__ is used!
:param _value_only: Internal parameter cannot be reached by standard access
* True - (default) in case of iDataModel items we delete only the internal value
not the model itself
* False - we delete the value independent from the type
:return: deleted value
"""
if _value_only:
try:
return super(iTData, self).__getitem__(key).clear(_it_data_model_identifier=0)
except (AttributeError, TypeError):
# AttributeError raised if clear() is not known
# TypeError raised if _it_data_model_identifier is not accepted
pass
return super(iTData, self).__delitem__(key)
def __copy__(self):
return iTData(super().copy())
def __deepcopy__(self):
iTData(copy.deepcopy(super()))
def __repr__(self):
# we represent via dict because dict will automatically load in again as iTreeData object
if self.is_empty:
return '%s()' % (self.__class__.__name__)
if self.is_no_key_only:
return '%s(%s)' % (self.__class__.__name__, repr(super(iTData, self).__getitem__(__NOKEY__)))
return '%s(%s)' % (self.__class__.__name__, super(iTData, self).__repr__())
def __hash__(self):
"""
Again hashing is quite slow here
:return: hash integer
"""
return hash((i for i in self.items()))
[docs] def update(self, E=None, **F):
"""
function update of multiple items
if one item is invalid the whole update will be skipped and an iDataValueError exception will thrown!
In case the replace_model flag is set the model will be exchanged.
Parameters taken from builtin dict:
Update D from dict/iterable E and F.
If E is present and has a .keys() method, then does:
If E is present and lacks a .keys() method, then does:
In either case, this is followed by:
:except: raises iDataValueError exception if a value in the given object is not matching to the data-model.
The iData object will not be updated in this case.
:param E:
* with .keys() method: for k in E: D[k] = E[k]
* without .keys() method: for k, v in E: D[k] = v
:param **F: we run: for k in F: D[k] = F[k]
:param replace_models:
* True - Will replace the whole key related value (also iTDataModels are replaced)
* False (default) - All values are replaced in case of iTDataModel object the internal value will
be replaced
"""
if F.get('replace_models') is True:
del F['replace_models']
helper = iTData(E, **F)
return super(iTData, self).update(helper.items())
# we first create a helper iTData object
helper = iTData(E, **F)
# check if we have just valid items will raise an exception if not matching!
# pre-check and model identification:
i = 0
super_class = super(iTData, self)
try:
models = deque()
for i, (k, v) in enumerate(helper.items()):
m_flag = False
if not isinstance(v, iTDataModel):
try:
super_class.__getitem__(k).validator(v)
m_flag = True
except (KeyError, AttributeError):
pass
models.append(m_flag)
except Exception as e:
raise e.__class__('Input item %s raises: %s' % (str(list(helper.items())[i]), str(e)))
# after pre check ran with success we finally fill in the data
[m and super_class.__getitem__(k).set(v) or super_class.__setitem__(k, v) for (k, v), m in
zip(helper.items(), models)]
[docs] def copy(self):
"""
create a new object with same items
:return: new object copied from self
"""
return self.__copy__()
[docs] def clear(self, values_only=False) -> None:
models = []
if values_only:
models = [((k, v), v.clear()) for k, v in super(iTData, self).items() if isinstance(v, iTDataModel)]
super().clear()
super().update([(k, v) for (k, v), _ in models])
[docs] def pop(self, key=__NOKEY__, default=__NOKEY__, value_only=True):
"""
delete a stored value
:except: will case KeyError if key is not found and default is not set
:param key: key where the item should be popped out
:default: define the value given back in case key is not found else
KeyError will be raised
:param value_only: True - only value will be deleted model will be kept in iTreeData
False - whole model will be popped out
:return: deleted item or default
"""
try:
item = super(iTData, self).__getitem__(key).clear(_it_data_model_identifier=0)
except KeyError:
if default != __NOKEY__:
return default
raise
except (AttributeError, TypeError):
# AttributeError raised if clear() is not known
# TypeError raised if _it_data_model_identifier is not accepted
return super(iTData, self).pop(key)
return item
[docs] def get(self, key=__NOKEY__, default=None, return_type=VALUE):
"""
get a specific data item by key
:param key: key of the data item (if not given __NOKEY__ is used)
:param default: default value that will be delivered in case of no match
:param _return_type: We can deliver different returns
* VALUE - value object
* FULL - iTreeDataModel (only if used else same as VALUE)
* STR - formatted string representation of the data value
:return: requested value
"""
try:
return self.__getitem__(key, _return_type=return_type)
except KeyError:
return default
# not supported methods:
[docs] def fromkeys(self, *args, **kwargs):
"""
create a new iData object based on given keys and optional value
- real signature unknown
"""
return iTData(dict.fromkeys(self, *args, **kwargs))
def __or__(*args, **kwargs):
"""
method not supported
:except: raises an Attribute error
"""
raise AttributeError('__or__() method not supported by iData object')
def __ior__(*args, **kwargs):
"""
method not supported
:except: raises an Attribute error
"""
raise AttributeError('__ior__() method not supported by iData object')
# additional methods (not available in normal dict)
[docs] def delete_item(self, key, value_only=True):
"""
delete a item by key
:except: KeyError is raised in case item key is unknown
:param key: key of the data item (if not given __NOKEY__ is used!
:param value_only:
* True - (default) in case of iDataModel items we delete only the internal value
not the model itself
* False - we delete the value independent from the type (also iDataModel objects)
:return: deleted value
"""
return self.__delitem__(key, value_only)
[docs] def model_values(self):
"""
iterator that takes in case of iDataModel values the value out of the model,
in case of non iDataModel values the value is given directly as it is
:return: iterator
"""
for v in super(iTData, self).values():
if isinstance(v, iTDataModel):
yield v.value
else:
yield v
[docs] def model_items(self):
"""
iterator that takes in case of iDataModel values the value out of the model,
in case of non iDataModel values the value is given directly as it is
:return: iterator
"""
for k, v in super(iTData, self).items():
if isinstance(v, iTDataModel):
yield k, v.value
else:
yield k, v
@property
def is_empty(self):
"""
used for identification of this class
:return: True
"""
return not self
@property
def is_no_key_only(self):
"""
used for identification of this class
:return: True
"""
return super(iTData, self).__len__() == 1 and super(iTData, self).__contains__(__NOKEY__)
@property
def is_iTData(self):
return True
[docs] def is_key_empty(self,key=__NOKEY__):
'''
Function delivers a key empty state (it delivers True in case key is absent or
value is __NOVALUE__
:param key: key to be check (delault is __NOKEY__
:return: True/False
'''
try:
return super(iTData, self).__getitem__(key) == __NOVALUE__
except KeyError:
return True
[docs] def deepcopy(self):
"""
create a deep copy of this object
also all internal items will be copied!
:return: new object deep copied from self
"""
return self.__deepcopy__()
[docs] def get_init_args(self):
return [item for item in self.items()]
[docs]class iTDataReadOnly(iTData):
"""
Standard itertree Data management object might be overloaded or changed by the user
"""
def __setitem__(self, *arg, **kwargs):
raise PermissionError('The iTDataReadOnly() object data can not be changed')
def __delitem__(self, *arg, **kwargs):
raise PermissionError('The iTDataReadOnly() object data can not be changed')
[docs] def pop(self, *arg, **kwargs):
raise PermissionError('The iTDataReadOnly() object data can not be changed')
[docs] def update(self, *arg, **kwargs):
raise PermissionError('The iTDataReadOnly() object data can not be changed')
[docs] def clear(self, *arg, **kwargs):
raise PermissionError('The iTDataReadOnly() object data can not be changed')
[docs] def delete_item(self, key, value_only=True):
raise PermissionError('The iTDataReadOnly() object data can not be changed')
def __repr__(self):
# we represent via dict because dict will automatically load in again as iTreeData object
return 'iTDataReadOnly(%s)' % super(iTData, self).__repr__()
def __copy__(self):
return iTDataReadOnly(super(iTData, self).copy())
def __deepcopy__(self):
iTDataReadOnly(copy.deepcopy(super(iTData, self).copy()))
[docs] def get_init_args(self):
return [item for item in self.items()]