# -*- 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 main iTree object
"""
from __future__ import absolute_import
import copy
import pickle
import traceback
from itertools import chain, dropwhile, zip_longest, takewhile, repeat, tee, product
from contextlib import suppress
from collections import OrderedDict, deque
from itertools import dropwhile
# import fnmatch
# import functools
# import operator
import sys
try:
from typing import Hashable, Iterable, Callable, Union, Optional
except ImportError:
# <Python 3.5
# Only required for doc strings!
Hashable = None
Iterable = None
Callable = None
Union = None
Optional = None
from .itree_helpers import itree_list, TagIdx, NoTarget, iTFLAG, iTLink, _iTFLAG, NoTag, \
NoValue, INF,BLIST_SWITCH
from .itree_serializer.itree_renderer import iTreeRender
from .itree_serializer.itree_json_serialize import iTStdJSONSerializer2
from .itree_indepth import _iTreeIndepthTree
from .itree_getitem import _iTreeGetitem
from .itree_private import _iTreePrivate
[docs]class iTree(_iTreePrivate):
__slots__ = ( # Attributes
'_tag', '_value', '_link', '_flags', '_coupled',
# private helper classes
'_itree_prt_idx', '_families', '_items',
# serializing objects
'__renderer', '__itree_serializer',
# quick access methods
'__len__', '__iter__', 'getitem_by_idx', '_is_list',
# quick access methods
'_setitem_list',
'_getitem_fam', '_setitem_fam',"_get_fam",
# helpers
'_hc_tree', 'get'
)
# we define some static private variables:
_filter_linked_roots = lambda i: i.is_link_root
_filter_linked_roots_and_linked = lambda i: i.is_link_root or i.is_linked
# flags
_READ_ONLY_TREE = iTFLAG.READ_ONLY_TREE
_READ_ONLY_VALUE = iTFLAG.READ_ONLY_VALUE
_LOAD_LINKS = iTFLAG.LOAD_LINKS
# internal flags
_LINKED = _iTFLAG.LINKED
_LINK_ROOT = _iTFLAG.LINK_ROOT
_PLACEHOLDER = _iTFLAG.PLACEHOLDER
_FLAG_MASK = _iTFLAG.FLAG_MASK
_IS_TREE_PROTECTED = _READ_ONLY_TREE | _PLACEHOLDER | _LINKED | _LINK_ROOT
_IS_VALUE_PROTECTED = _READ_ONLY_VALUE | _PLACEHOLDER | _LINKED
_DEEP_FLAG_MASK = _LOAD_LINKS | _READ_ONLY_TREE
_NoneSlice = slice(None)
_ONE_ITEM_LIST = itree_list([0]) # used for quickest possible instance of blist
[docs] def __init__(self,
tag=NoTag, # family tag (any hashable object)
value=NoValue, # data object to be stored in the item
subtree=None, # subtree definition
link=None, # link to another iTree
flags=0, # property flags
):
"""
This is the main class related to itertree module. It represents the node in the nested tree structure.
In case the object contains a subtree this object is the parent of the children in the subtree and its inner
children (children, sub-children, etc.). The ´iTree´-object itself can also be a
child of a parent ´iTree´-object. If this is not the case the ´iTree´-object is the root of the tree.
*Limitation:* An ´iTree´-object can be integrated as a child in one ´iTree´ only (one parent only principle)!
Each ´iTree´-object contains a "tag". The objects tag can be any hashable object.
Different as dictionaries it is allowed to put multiple items with the same tag inside the ´iTree´.
Those items with
the same tag are placed and ordered (enumerated) in the related tag-family. The specific items can be targeted
via a zag_idx tuple (family-tag,family-index) which is the items unique key.
Linked ´iTree´-objects will behave a bit special. They have a read only structure (children) and they contain
the children (tree) of the linked ´iTree´.
The "local" attributes like tag, value, ... can be set independent of the linked item (local properties).
To change the tree structure of such an object you can change the original link target. But an explicit reload
( ´load_links()´ ) is required to get the change active in the linked items.
Beside the linked item the user can add local items and mix them with the linked ones. But the general
structure is always determined by the linked in children.
Beside the subtree the ´iTree´-object can also contain a value. The value can be any type of Python objects
that is stored in the tree-node (comparable with the value of a dictionary). If it is required by the user to
calculate the hash
of the íTree´ via ´hash(item)´ some value objects might not be hashable and will raise an exception. But as long
as the objects can be pickled a hash replacement will be found in the hash of ´iTree´. E.g. a dict placed
as a value makes no troubles even if teh user likes to hash the tree.
As a helper the ´iTree´-object can be coupled with other objects (which might be helpful if you have a displayed
tree in a GUI that is connected with the ´íTree´. Be aware that this helper function has only temporary
character.
It is not stored when dumping (standard dump) or considered in comparisons, etc. The coupled object is
ignored by all internal functionalities. Also in linked items the coupled object is not taken over from the link
and can be set independent.
The behavior of a íTree´ object can be influenced by specific properties or flags:
* Read-only tree: An ´iTree´ object where the subtree is protected and cannot be changed
* Read-only value: An ´iTree´ object where the value is protected and cannot be changed
The ´iTree´ object contains a large number of properties which should help the user to reach the
required information as comfortable as possible. Especially the tree related information might be interesting:
* mytree.tag -> family-tag of the item
* mytree.idx -> absolute index of the object
* mytree.tag_idx -> key tuple (family-tag, family-index)
* mytree.idx_path -> tuple of absolute indexes from the root to the item
* mytree.tag_idx_path -> tuple of key-tuples from the root to the item
* mytree.parent -> parent item of the item
* mytree.root -> root item of the item (highest level parent)
* mytree.pre_item -> pre item (the children in the parent that is before this item)
* mytree.post_item -> post item (the children in the parent that is after this item)
* mytree.level -> How deep the item is in the tree related to the root
* mytree.max_depth -> How deep the sub-items (nested) of the ´iTree´ go in maximum (deep levels)
In case the ´iTree´ object is not part of another ´iTree´ (is root) those attributes
will deliver in most cases ´None´.
* mytree.is_root -> True in case the item is a root ´iTree´)no parent)
* mytree.is_tree_read_only -> True in case the subtree is protected and read-only
* mytree.is_value_read_only -> True in case the item value is protected and read-only
* mytree.is_linked -> True in case the item is a linked item (read_only)
* mytree.is_link_root-> True in case the item is a root for a link to another ´iTree´
* mytree.link_root-> Delivers the related link-root in case the item is linked
* mytree.value-> Delivers the value object stored in the ´iTree´ item
There are different ways to access the children and sub-children in the tree of a ´iTree´ object.
The standard access for single items is via ´itree_obj[target]´ ( ´__getitem__(target)´) call.
As targets the user has different options:
* index - absolute target index integer (fastest operation)
* key - key tuple (family_tag, family_index)
* tag or tag sets- family_tag object targeting a whole family
* target-list - absolute indexes or keys to be replaced (indexes and keys can be mixed)
* index slice - slice of absolute indexes
* key slice - tuple of (family_tag, family_index_slice)
* filter-method - method to filtering specific children
Beside the first level functions the `iTree`-object contains the helper class `.deep` which contains
the in-depth functionalities targeting all the nested sub-children of the object.
As the name itertree should suggest a wide range of iteration methods are available in the class.
They can be combined with different kind of filters.
.. _filter_method:
.. note:: As optional `filter_method`-parameter the user can give:
* `None`- filter inactive
* `Callable` delivering `True`/`False` related to a characteristic of the
`ìTree`-object (iterated items)
Beside this the internal filtering is normally a hierarchical filtering (If the parent does not match
to the filter all children are excluded too, even that they match to the filter). Some methods contain
a switch
for non-hierarchical filtering too. But most often the non-hierarchical filtering can be realized via the
build-in `filter()` method and in this case the switch is not available.
Here the power of the iterators is obvious because cascaded filter queries can be constructed and finally in
only **one** full iteration over all the items is required to get the results back
(sometimes the full iteration is not required).
It's recommended to have a look into itertools package for better usage of the delivered iteration-generators.
The design of the ´iTree´´ object is made for best possible performance even that it is pure Python.
Some part of
the code might look less good readable or in the iteration-generators you find the if else outside the
iteration functionality which is not realized via sub-functions we have here redundant codings.
But its is made to avoid conditions or function calls inside the loops which would be bad for the performance.
:type tag: Hashable
:param tag: family tag of the iTree object (any hashable object)
:type value: object
:param value: value to be stored in the iTree object
:type subtree: Optional[Iterable]
:param subtree: Iterable or Iterator containing the subtree items or an argument list (internal functionality)
:type link: Optional[iTLink]
:param link: iTLink object targeting another iTree
:type flags: int
:param flags: flags taken from iTFLAG class:
* iTFLAG.READ_ONLY_TREE - mark the subtree of this iTree as read-only
the subtree will be protected from changes in this case
* iTFLAG.READ_ONLY_VALUE - mark the value of this iTree object as read-only
* iTFLAG.LOAD_LINKS - load the links during instance automatically
Multiple flags can be combined via `|`
"""
# handle family tag:
if not tag.__hash__:
raise TypeError('Given tag is not hashable')
self._tag = tag
# set data value
self._value = value
# flags
if flags:
self._flags = flags = flags & self._FLAG_MASK
else:
self._flags = 0
# _itree_prt_idx
# 1. hasattr((o,'_itree_prt_idx') -> Used for class/object identification ->
# 2. o._iter_parent is None -> iTree-object is not part of a parent iTree
# 3. o._itree_prt_idx is list -> iTree-object is part of a iTree
# list: [parent_itree_object, absolute_index_cache, family_index]
# a. o._itree_prt_idx[0] -> iTree parent-object
# b. o._itree_prt_idx[1] -> cached absolute index in iTree parent-object (might be outdated!)
# c. o._itree_prt_idx[3] -> cached family index in iTree parent-object (might be outdated!)
self._itree_prt_idx = None
self.get = getitem = _iTreeGetitem(self)
# internals like self._families are created only in case elements added to the object
# load the subtree
if subtree:
# we only mask out the flags that should be brought into the deeper levels!
if flags:
sl = list(self._iter_extend(self, subtree, flags & self._DEEP_FLAG_MASK, init=True))
else:
sl = list(self._iter_extend(self, subtree, init=True))
self._items = sl = itree_list(sl)
else:
self._items = sl = []
self.getitem_by_idx = getitem.getitem_by_idx = sl.__getitem__
self.__len__, self.__iter__ = sl.__len__, sl.__iter__
# links
if link:
t = type(link)
if t is iTLink:
# we create a new object -> to be sure that we have a virgin iTLink-object
link = link.get_args()
elif t is tuple and len(link) > 4:
raise TypeError('Given link %s not supported!' % repr(link))
self._link = iTLink(*link)
self._flags = self._flags | self._LINK_ROOT
# load links
if flags & self._LOAD_LINKS:
self.load_links()
# *** parent related properties and methods ************************************************************************
@property
def parent(self):
"""
Property delivers current items parent-object.
:rtype: Union[iTree, None]
:return: iTree parent-object or None (in case no parent exists)
"""
# Implementation state: ready, tested, doc ok
return self._itree_prt_idx[0] if self._itree_prt_idx else None
@property
def is_root(self):
"""
Is this item a root-item (has no parent)?
:rtype: bool
:return:
* *True* - is root
* *False* - is not root
"""
# Implementation state: ready, tested, doc ok
return self._itree_prt_idx is None
@property
def root(self):
"""
property delivers the root-item of the tree
In case the item has no parent it will deliver itself
:rtype: iTree
:return: iTree root item
"""
# Implementation state: ready, tested, doc ok
p = self
while p is not None:
root, p = p, p.parent
return root
@property
def tag(self):
"""
This is the access to the object-tag. The tag gives the relation to the tag-family in `iTree`-objects.
The tag is comparable with a key in dictionaries but in iTrees the tag is not unique! For unique iTree
identification the `tag_idx` property must be used.
Any hashable object can be used as a tag, but in case "exotic" objects are used and serialization is required
the user may have to extend the functionality of the serializer.
:rtype: Hashable
:return: tag - hashable object giving the family relation
"""
# Implementation state: ready, tested, doc ok
return self._tag
@property
def idx(self):
"""
Index of this object in the iTree (related to the absolute order)
*Method is very important for internal functionalities*
.. note::
In general the item index is cached but in case of deleted items or reorder operations
the cache might be outdated. In this case the index update based on a search might take longer.
:rtype: Union[int, None]
:return: unsigned integer representing the index (related to absolute order of iTree)
"""
# Implementation state: ready, tested, doc ok
parent_list = self._itree_prt_idx
if parent_list:
parent, abs_idx, _ = parent_list
siblings = parent._items
# create locals for multi use functions
size = len(siblings)
if abs_idx < size and siblings[abs_idx] is self:
# cache matches
return abs_idx
# cache index must be updated
# search in near area
delta = 20
limit = min(size, abs_idx + delta + 1)
start = max(0, abs_idx - delta)
for i in range(start, limit):
if siblings[i] is self:
parent_list[1] = i
return i
if abs_idx < size / 2:
# start -> end
i = 0
for item in siblings:
item._itree_prt_idx.__setitem__(1, i)
if item is self:
return i
i = i + 1
raise IndexError('Internal error for this iTree we found no related index in the parent-object!')
else:
# end -> start
for i in range((size - 1), -1, -1):
item = siblings[i]
item._itree_prt_idx.__setitem__(1, i)
if item is self:
return i
raise IndexError('Internal error for this iTree we found no related index in the parent-object!')
@property
def idx_path(self):
"""
delivers a list of absolute indexes from the root to this item
For items with no parent (root_item) an empty tuple will be delivered
.. note::
We deliver here a tuple because it might be helpful if the object is hashable
(usage as a dict key)
:rtype: tuple
:return: tuple of index integers (here we do not deliver an iterator!)
"""
# Implementation state: ready, tested, doc ok
p = self
idx_list = deque()
while 1:
root, p = p, p.parent
if p is None:
break
else:
idx_list.appendleft(root.idx)
return tuple(idx_list)
@property
def tag_idx(self):
"""
The tag_idx is a unique identification of the item. It is represented by a tuple containing the family-tag
and the family related index of the item.
If the item is not part of a parent-tree (root-item) in this case the result will be `None`.
:rtype: Union[tuple, None]
:return: tuple (family-tag, family-index) or None (if item has no parent)
"""
parent_list = self._itree_prt_idx
if parent_list:
parent = parent_list[0]
getitem_fams = parent._getitem_fam
tag = self._tag
# we use cached index to be quicker
family_idx = parent_list[2]
family = getitem_fams(tag)
fm_getitem = family.__getitem__
size = family.__len__()
if family_idx < size and fm_getitem(family_idx) is self:
return tag, family_idx
delta = 20
limit = min(size, family_idx + delta + 1)
start = max(0, family_idx - delta)
for i in range(start, limit):
if fm_getitem(i) is self:
parent_list[2] = i
return tag, i
# update the whole list
if family_idx < size / 2:
# start -> end
i = 0
for item in family:
item._itree_prt_idx.__setitem__(2, i)
if item is self:
return i
i = i + 1
raise IndexError('Internal error for this iTree we found no related index in the parent-object!')
else:
# end -> start
for i in range((size - 1), -1, -1):
item = family[i]
item._itree_prt_idx.__setitem__(2, i)
if item is self:
return i
raise IndexError('Internal error for this iTree we found no related index in the parent-object!')
@property
def tag_idx_path(self):
"""
The path is a tuple of tag_idx tuples from root to this item.
Each tag_idx is a tuple containing the pair family-tag and family-index.
For items with no parent (rooot_item) an empty tuple will be delivered
.. note::
We deliver here a tuple because it might be helpful if the object is hashable
(usage as a dict key)
:rtype: tuple
:return: tuple of key tuples containing family-tag and family-index
"""
# Implementation state: ready, tested, doc ok
p = self
key_list = deque()
while 1:
root, p = p, p.parent
if p is None:
break
else:
key_list.appendleft(root.tag_idx)
return tuple(key_list)
[docs] def force_cache_update(self, idx=True, fam_keys=True, all_keys=True):
"""
Forces the update of the index and keys in cache
Normally this is not required the methode is mainly used for testing proposes
:param idx: True - update absolute-indexes
:param fam_keys: True - update this items family-indexes
:param all_keys: True - update all families faimily-indexes
"""
parent_list = self._itree_prt_idx
if parent_list:
parent = parent_list[0]
if idx:
list(item._itree_prt_idx.__setitem__(1, i) for i, item in enumerate(parent))
if fam_keys and not all_keys:
family = self._getitem_fam(self._tag)
list(item._itree_prt_idx.__setitem__(2, i) for i, item in enumerate(family))
elif all_keys:
for family in parent._families.values():
list(item._itree_prt_idx.__setitem__(2, i) for i, item in enumerate(family))
@property
def pre_item(self):
"""
Delivers the pre-item (predecessor) of this object in the parent-tree.
If self is first item or there is no parent `None` will be delivered.
:rtype: Union[iTree,None]
:return: iTree predecessor or None (no match)
"""
# Implementation state: ready, tested, doc ok
if self._itree_prt_idx is None:
return None
idx = self.idx - 1
return None if idx < 0 else self._itree_prt_idx[0]._items.__getitem__(idx)
@property
def post_item(self):
"""
Delivers the post-item (successor) of this object in the parent-tree.
If self is first item or there is no parent `None` will be delivered.
:rtype: Union[iTree,None]
:return: `iTree` successor or `None` (no match)
"""
# Implementation state: ready, tested, doc ok
if self._itree_prt_idx is None:
return None
idx = self.idx + 1
sl = self._itree_prt_idx[0]._items
return sl.__getitem__(idx) if idx < sl.__len__() else None
@property
def level(self):
"""
Delivers the distance (number of levels) to the root-item of the tree. Or in other words how
deep in tree the item is positioned.
In case item has no parent (is a root-item) this method will deliver 0.
:rtype: int
:return: integer - number of levels (outer direction)
"""
# Implementation state: ready, tested, doc ok
i = 0
pt = self._itree_prt_idx
while (pt):
p = pt[0]
i = i + 1
pt = p._itree_prt_idx
return i
@property
def max_depth(self):
"""
Relative from this item the method measures the maximum depth of the tree and delivers
the maximum number of levels that are found in this object.
If the user wants to now the maximum depth of the whole tree ensure that the property of the root-item is read.
The user might use `my_tree.root.max_depth` to ensure this.
:rtype: int
:return: integer maximal number of levels that exists in the tree (inner direction)
"""
# Implementation state: ready, tested, doc ok
if not self:
return 0
max_depth = 0
items = [self]
while 1:
new_items = []
deque((new_items.extend(list(i)) for i in items), maxlen=0)
if len(items) == 0:
break
else:
max_depth += 1
items = new_items
return max_depth - 1
@property
def tag_number(self):
"""
property contains the number of tags (families) the itree contains
:return: integer
"""
if self:
return len(self._families)
else:
return 0
# *** properties targeting internal sub/helper classes *************************************************************
@property
def deep(self):
"""
Subclass containing the deep access to the nested structures of iTree
:return:
"""
try:
return self._hc_tree
except AttributeError:
# The subclass is only instanced if it is first used
tree, tree._itree, tree.get = _iTreeIndepthTree(), self, self.get
self._hc_tree = tree
return tree
# flags
@property
def flags(self):
"""
Give the flags value of the object. The integer value stored in this property contains the bit flags
related to the constants iTFLAG or _iTFLAG.
To see the details the user might use `bin()` or the helper property `flags_repr` which delivers a
string containing all set flags.
;rtype: int
:return: The flags set for this item
"""
# Implementation state: ready, tested, doc ok
return self._flags
[docs] def flags_repr(self, public_only=True):
"""
String representation of flags for this item
:type public_only: bool
:param public_only:
* True - Consider only the public flags (given by the user) -> default
* False - Show all flags (also linked and placeholder flags)
;rtype: str
:return: String repr of the flags set for this item
"""
# Implementation state: ready, partly tested, doc ok
out = []
if self._flags & self._READ_ONLY_TREE != 0:
out.extend(('iTFLAG.READ_ONLY_TREE', '|'))
if self._flags & self._READ_ONLY_VALUE != 0:
out.extend(('iTFLAG.READ_ONLY_VALUE', '|'))
if self._flags & self._LOAD_LINKS != 0:
out.extend(('iTFLAG.LOAD_LINKS', '|'))
if not public_only:
if self._flags & self._LINKED != 0:
out.extend(('_iTFLAG.LINKED', '|'))
if self._flags & self._LINK_ROOT != 0:
out.extend(('_iTFLAG.LINK_ROOT', '|'))
if self._flags & self._PLACEHOLDER != 0:
out.extend(('_iTFLAG.PLACEHOLDER', '|'))
return ''.join(out).rstrip('|')
@property
def is_tree_read_only(self):
"""
Is the tree protection flag set? In this case the tree structure cannot be changed
This property targets the tree structure not the value!
:rtype: bool
:return:
* False - subtree can be changed (writeable)
* True - subtree is protected (read-only)
"""
# Implementation state: ready, tested, doc ok
return bool(self._flags & (self._READ_ONLY_TREE | self._LINKED))
[docs] def set_tree_read_only(self):
"""
Set the tree protection flag. If the flag is set the subtree structure can not be changed anymore.
.. Warning:: Setting the structural protection is always a deep operation. In all children and sub-children
the protection flag will be activated too! But when unset the behavior it is not automatically
made as a deep operation`. Here the differentiation in between the two methods
`unset_tree_read_only()` and `unset_tree_read_only_deep()` exists.
"""
# Implementation state: ready, tested, doc ok
if self._flags & self._LINKED:
_iTreePrivate._raise_read_only_exception(self)
set_flags = _iTreePrivate._set_flags
read_only_tree_flag = self._READ_ONLY_TREE
set_flags(self, read_only_tree_flag)
for i in self.deep:
set_flags(i, read_only_tree_flag)
[docs] def unset_tree_read_only(self):
"""
Unset the tree protection flag on the item. Only the children structure of this item is made
writable by this operation.
:except: If the parent contains the tree protection flag a PermissionError will be raised
"""
# Implementation state: ready, tested, doc ok
if self._itree_prt_idx is not None and self._itree_prt_idx[0]._flags & self._READ_ONLY_TREE:
raise PermissionError('The structural protection flag can only be unset in '
'case the parent is not protected. But here the parent holds the protection flag')
self._unset_flags(self, self._READ_ONLY_TREE)
@property
def is_value_read_only(self):
"""
Is iTree value read_only? Is the value protection flag iTFLAG.READ_ONLY_VALUE is set?
:rtype: bool
:return:
True - read-only protection of value active
False - value is writeable
"""
# Implementation state: ready, tested, doc ok
return bool(self._flags & (self._READ_ONLY_VALUE | self._LINKED))
[docs] def set_value_read_only(self):
"""
Set the write protection of the value (set flag: iTFLAG.READ_ONLY_VALUE)
"""
# Implementation state: ready, tested, doc ok
self._set_flags(self, self._READ_ONLY_VALUE)
[docs] def unset_value_read_only(self):
"""
Unset the write protection flag of the value (set flag: iTFLAG.READ_ONLY_VALUE).
Value will be writeable afterwards
"""
# Implementation state: ready, tested, doc ok
if self._flags & self._LINKED:
_iTreePrivate._raise_read_only_exception(self)
self._unset_flags(self, self._READ_ONLY_VALUE)
# *** value and coupled object related properties/methods **********************************************************
@property
def value(self):
"""
Delivers the full value object stored in the `iTree`-object
:rtype: object
:return: value-object of the item
"""
# Implementation state: ready, tested, doc ok
return self._value
[docs] def set_value(self, value):
"""
Set/replace the value content of the `iTree`-object.
The method returns the previous stored value object that was replaced by the operation.
.. note:: If an `iTValueModel` is stored as value in the `iTree` by default the set_value() method will
target the value which is stored inside the model. If the model itself should be exchanged the
user must
give the new model as value parameter of this method. To replace the model with another Python
object the user must first delete the model via `del_value()` command and afterwards set the new value.
:type value: object
:param value: data-object that should be placed as value or in case we
have a `iTValueModel` already as value it is placed inside the model.
:rtype: object
:return: old value object that was stored in iTree before
"""
# Implementation state: ready, tested, doc ok
if self._flags & self._IS_VALUE_PROTECTED:
raise PermissionError('iTree value is read only')
old_value = self._value
# do we have a model?
if (
hasattr(old_value, 'is_iTValueModel')
and hasattr(value, 'is_iTValueModel')
or not hasattr(old_value, 'is_iTValueModel')
):
# new model given!
self._value = value
else:
old_value = old_value.set(value)
return old_value
[docs] def set_key_value(self, key, value):
"""
Depending on the already stored object this operation is a sub-replacement of a part only.
The method returns the previous stored value object that was replaced by the operation.
The user can influence the behavior by giving the `key` parameter. And it depends on the already
stored value object (e.g. a `list` or `dict` ). Only the value of the related
item will be replaced or in case the item did not exist yet the might object will be extended by the
given value ( `dict` only).
Depending on given key parameter and the already stored object we have the following possible behaviours:
* dict stored in value -> store the value in the dict with the key given in key_index
* dict stored in value and matching item-value is a `iTValueModel` -> replace value inside the model
* list stored in value -> key_index must be an index and replace the related item in the list with the
value given
* list stored in value and matching (index) item-value is a `iTValueModel` -> replace value inside the model
* key == `INF` and list stored in value -> append given value in the list
.. note:: If an `iTValueModel` is stored as value in the `iTree` by default the `mytree.set_value()`-method will
target the value which is stored inside the model. If the model itself should be exchanged
the user must give a new model as value parameter of this method. To replace the model with another
Python object the user must first delete the model via `del mytree.value[key]` command and afterwards
set the new value or he sets the value directly via `mytree.value[key]==new_value` .
:type key: Optional[Hashable,int]
:param key: key or index of the value object (depends on the object already stored in `iTree` ).
if `key==INF` the value will be appended in case a list-like object is already
stored in the `iTree`-object.
:type value: object,
:param value: value object that should be placed as value or in case a key is given the sub-value in
the `iTree`
or in case we have a `iTValueModel` is used inside the model.
:rtype: object
:return: old value object that was stored in iTree before
"""
# Implementation state: ready, tested, doc ok
if self._flags & self._IS_VALUE_PROTECTED:
raise PermissionError('iTree value is read only')
old_value = self._value
try:
old_value = old_value[key]
except KeyError:
old_value[key] = value
return NoValue
except TypeError:
if key == INF:
old_value.append(value)
return NoValue
raise
# do we have a model?
if (
hasattr(old_value, 'is_iTValueModel')
and hasattr(value, 'is_iTValueModel')
or not hasattr(old_value, 'is_iTValueModel')
):
# new model given!
self._value[key] = value
else:
old_value = old_value.set(value)
return old_value
[docs] def get_value(self):
"""
Delivers the value-object of the item or a sub-value in case key_index parameter is used and a
matching object is stored in the `iTree` .
.. note:: If `iTValueModel` is stored in `iTree` the method will not target the model it will
target the value inside. If the model itself is required the `value`-property of `iTree` must be used.
:except: In case a key_index is given but the object is not a `dict` or a `list` like object an `AttributeError`
will be raised ( `__getitem__()`required). If no matching item is found an
`IndexError` or `KeyError` will be raised.
:rtype: object
:return: value object the `iTree` or `iTValueModel` (in case a model is stored in the `iTree` )
"""
# Implementation state: ready, tested, doc ok
value = self._value
return value.value if hasattr(value, 'is_iTValueModel') else value
[docs] def get_key_value(self, key):
"""
Delivers the value-object of the item or a sub-value in case key_index parameter is used and a
matching object is stored in the `iTree` .
In case the stored value is a `dict`-like object the key will be used as the key of the dict.
In case the stored value is a `list`-like object the keyx will be used as the index of the list.
In case the target value is a `iTValueModel` the value inside will be targeted and not the model itself.
.. note:: If `iTValueModel` is stored in `iTree` the method will not target the model it will
target the value inside. If the model itself is required the `value`-property of `iTree` must be used.
:except: In case a key_index is given but the object is not a `dict` or `list` like object an `AttributeError`
will be raised ( `__getitem__()`-method required). If no matching item is found an
`IndexError` or `KeyError`
will be raised.
:type key: Optional[Hashable,int]
:param key: Optional key or index parameter
:rtype: object
:return: value object the `iTree` or `iTValueModel` (in case a model is stored in the `iTree`)
"""
# Implementation state: ready, tested, doc ok
value = self._value[key]
return value.value if hasattr(value, 'is_iTValueModel') else value
[docs] def del_value(self):
"""
Deletes the full value-object stored in ´iTree´ ( ´NoValue´ is stored in iTree).
This method will always delete the whole object stored in `iTree` even `iTValueModel`-objects are deleted. To
delete the value content of a model `mytree.value.clear()` or 'set_value(NoValue)' might be used.
:return: deleted value
"""
# Implementation state: ready, tested, doc ok
if self._flags & self._READ_ONLY_VALUE:
raise PermissionError('iTree value is read only')
old_value, self._value = self._value, NoValue
return old_value
[docs] def del_key_value(self, key):
"""
If no parameter is given deletes the full value-object stored in ´iTree´ (store ´NoValue´ ).
In case a key or index is given and the value contains a matching object we will only pop out the
related sub-item.
This method will always delete the whole targeted object even `iTValueModel`-objects are deleted. To
delete the value content of a model `mytree.value.clear()` or 'set_value(NoValue)' might be used.
:except: In case a key is given but the object is not `dict` or `list` like a TypeError or AttributeError
will be raised
( `__delitem__()`-method is targeted);
If the given key does not exist or an invalid parameter is given a KeyError or IndexError
will be raised.
:type key: Optional[Hashable,int]
:param key: Optional key or index to exchange just sub-items in the value
:return: deleted value
"""
# Implementation state: ready, tested, doc ok
if self._flags & self._READ_ONLY_VALUE:
raise PermissionError('iTree value is read only')
return self._value.pop(key)
@property
def coupled_object(self):
"""
The `iTree`-object can be coupled with another Python-object. The pointer to the object is stored and can be
reached via this property. (E.g. this can be helpful when connecting the `iTree` with a visual item
(hypertree-list item) in a GUI)
:return: pointer to coupled-object or None if no object is stored
"""
# Implementation state: ready, tested, doc ok
try:
return self._coupled
except AttributeError:
return None
# set properties
[docs] def set_coupled_object(self, coupled_object):
"""
Couple another Python-object with this `iTree`-object.
Compared with the `value` the coupled-object is not tracked by any internal functions. We do not consider
it in any relation (e.g. `__contains__()` and do not dump it in files, etc. Even in linked items the
coupled-object is not protected. And in copies it is ignored and not taken over.
.. note:: E.g. The coupled-object might be an object in a GUI that is related to this item.
:param coupled_object: object pointer to the object that should be coupled with this iTree item
"""
# Implementation state: ready, tested, doc ok
self._coupled = coupled_object
# *** structure related functions **********************************************************************************
# setters:
[docs] def append(self, item=NoValue):
"""
Append the given `iTree`-object to the `iTree` (new last child)
The `append()` method is the fastest way to add a single item to the end of the tree.
:except: In case `iTree`-object has already a parent a `RecursionError` will be raised
Other exceptions might come up in case the `iTree` is protected (tree read-only mode).
:type item: Union[iTree,object]
:param item: `iTree`-object to be appended
.. warning::
In case the given item-object is not a `iTree`-object the item is interpreted
as a value and the `iTree` will be created implicit (with tag-family `NoTag`) in the way:
`iTree(tag=NoTag, value=item)` ~ ìTree(value=item)
If no item is given an empty iTree is created tag=`NoTag`; value=`NoValue`.
>>> root=iTree('root')
>>> root.append('myvalue')
iTree(value='myvalue')
>>> root.append() # append an empty iTree-object
iTree()
:rtype: iTree
:return: Delivers the appended item itself
(it might be useful for the user to get the updated information of the object).
"""
if self._flags & self._IS_TREE_PROTECTED:
if self.is_link_root:
if hasattr(item, '_itree_prt_idx') and item.flags & (
self._LINKED | self._LINK_ROOT | self._PLACEHOLDER):
raise TypeError('Linked items cannot be appended to linked item as local item')
else:
self._raise_read_only_exception(self)
try:
if item._itree_prt_idx is not None:
raise RecursionError('Given item has already a parent iTree!')
tag = item._tag
except AttributeError:
# implicit definition of iTree:
item = iTree(value=item)
tag = NoTag
# return self._append_item(self,item)
# Just for performance we keep the code for append here and do not use the helper
abs_idx = len(self) # after tests here the len() is quicker (not understood why)
if abs_idx:
self._items.append(item)
# append item to family
family = self._get_fam(tag)
if family is None:
self._setitem_fam(tag, [item])
item._itree_prt_idx = [self, abs_idx, 0]
else:
fm_idx = family.__len__() # after tests here the .>__len__ is quicker (not understood why)
family.append(item)
if fm_idx==BLIST_SWITCH:
self._setitem_fam(tag,itree_list(family))
item._itree_prt_idx = [self, abs_idx, fm_idx]
else:
# here we must init all family and item related attributes
getitem = self.get
# items
self._items = sl = self._ONE_ITEM_LIST.copy()
sl[0] = item
self.getitem_by_idx = getitem.getitem_by_idx = sl.__getitem__
self.__len__, self.__iter__, self._setitem_list = sl.__len__, sl.__iter__, sl.__setitem__
# family
self._families = families = {tag: sl.copy()}
getitem._getitem_fam=self._getitem_fam=families.__getitem__
self._get_fam, self._setitem_fam = families.get, families.__setitem__
item._itree_prt_idx = [self, 0, 0]
return item
def __iadd__(self, other):
"""
append the given item to the iTree (short form of `append()`)
:except: In case `iTree`-object has already a parent a `RecursionError` will be raised
Other exceptions might come up in case the `iTree` is protected (tree read-only mode).
:type other: Union[iTree,object]
:param other: `iTree`-object to be appended.
.. warning::
As in `append()` in case the given item-object is not a `iTree`-object the item is interpreted
as a value and the `iTree` will be created implicit (with `NoTag` tag).
:rtype: `ìTree`
:return: self
"""
# Implementation state: ready, tested, doc ok
self.append(other)
return self
[docs] def appendleft(self, item=NoValue):
"""
Append the given `iTree`-object to the left of the parent-tree (new first child)
The `appendleft()` method is the recommended method to add a new first item to iTree
(quicker than `insert(0,item)` ).
Compared to `append()` the method is slower and the cache index information gets invalid after the operation
(will be automatically updated later on if required).
:except: In case `iTree`-object has already a parent a `RecursionError` will be raised.
Other exceptions might come up in case the `iTree` is protected (tree read-only mode).
:type item: Union[iTree,object]
:param item: `iTree`-object to be appended as first item.
.. warning::
As in `append()` in case the given item-object is not a `iTree`-object the item is interpreted
as a value and the `iTree` will be created implicit.
:rtype: iTree
:return: Delivers the appended item itself
(it might be useful for the user to get the updated information of the object).
"""
# Implementation state: ready, tested, doc ok
flags = self._flags
if flags & self._IS_TREE_PROTECTED:
if self.is_link_root:
# if self is link_root and the tag of the given item is different then the linked
# ones operation is allowed!
if hasattr(item, '_itree_prt_idx'):
if item.flags & (self._LINKED | self._LINK_ROOT | self._PLACEHOLDER):
raise TypeError('Linked items cannot be appended to linked item as local item')
if self._link.is_loaded and item.tag in self._link.tags:
self._raise_read_only_exception(self)
elif self._link.is_loaded and NoTag in self._link._tags:
self._raise_read_only_exception(self)
else:
self._raise_read_only_exception(self)
try:
if item._itree_prt_idx is not None:
raise RecursionError('Given item has already a parent iTree!')
except AttributeError:
# implicit definition of iTree:
item = iTree(value=item)
if self:
return self._append_item_left(self, item)
else:
return self._append_item(self, item)
[docs] def insert(self, target, item=NoValue):
"""
Insert an item **before** a given target-position. The insertion works like in lists.
The insertion operation is slower as the append operations.
If `target=None` is given the operation inserts in the last position (== `append()`).
:except: In case `iTree`-object has already a parent a `RecursionError` will be raised
Other exceptions might come up in case the `iTree` is protected (tree read-only mode).
:type target: Union[Integer,tuple,iTree,None]
:param target: target position definition; **target must target a single/unique item!**
Possible targets:
* index - absolute target index integer, negative values supported too (count from the end).
* key - key-tuple (family_tag, family_index) pair
* item - `iTree`-item that is already a children (future successor)
* None - if `None` is given we will append the item in the last position of the ´iTree´-object
:type item: Union[iTree,object]
:param item: `iTree`-object to be inserted in the tree.
.. warning::
As in `append()` in case the given item-object is not a `iTree`-object the item is interpreted
as a value and the `iTree` will be created implicit.
:rtype: iTree
:return: Delivers the inserted item itself
(it might be useful for the user to get the updated information of the object).
"""
# Implementation state: ready, tested, doc ok
if target is None:
return self.append(item)
flags = self._flags
if flags & self._IS_TREE_PROTECTED:
if self.is_link_root:
# if self is link_root and the tag of the given item is different then the linked
# ones operation is allowed!
if hasattr(item, '_itree_prt_idx'):
if item.flags & (self._LINKED | self._LINK_ROOT | self._PLACEHOLDER):
raise TypeError('Linked items cannot be appended to linked item as local item')
if self._link.is_loaded and item.tag in self._link._tags:
self._raise_read_only_exception(self)
elif self._link.is_loaded and NoTag in self._link._tags:
self._raise_read_only_exception(self)
else:
self._raise_read_only_exception(self)
try:
if item._itree_prt_idx is not None:
raise RecursionError('Given item has already a parent iTree!')
except AttributeError:
# implicit definition of iTree:
item = iTree(value=item)
if self:
sl = self._items
size = sl.__len__()
if size == 0 and target != 0:
raise KeyError('iTree is empty no valid target given!')
# absolute index of the target
if type(target) is int: # is already the absolute index!
abs_idx = size + target if target < 0 else target
elif hasattr(target, '_itree_prt_idx'):
if target._itree_prt_idx is not self:
raise ValueError('Given target is not part of the iTree')
abs_idx = target.idx
else:
abs_idx = self.__getitem__(target).idx
if abs_idx == 0:
return self._append_item_left(self, item)
# insert in list
sl.insert(abs_idx, item)
# insert item to family
tag = item._tag
family = self._get_fam(tag)
if family is None:
self._setitem_fam(tag, [item])
item._itree_prt_idx = [self, abs_idx, 0]
else:
fm_idx = self._get_family_insertion_idx(family, abs_idx)
family.insert(fm_idx, item)
if fm_idx==BLIST_SWITCH:
self._setitem_fam(tag,itree_list(family))
item._itree_prt_idx = [self, abs_idx, fm_idx]
return item
else:
# first item insert is append
return self._append_item(self, item)
# multiple appends
[docs] def extend(self, items):
"""
We extend the `iTree` with given items (multi append). The function is high performant and if you have to
append a large number of items it is recommended to create an iterator of the items and
feed them into this method. This is quicker compared to a loop doing multiple normal `append()` operations.
.. note:: In case the to be extended items have already a parent an implicit copy will be made.
We do this because the internal copy can be created more effective.
We accept also iTree-objects as extend_items parameter and the children which have a parent will be
automatically copied to be integrated in this second tree. We have the same situation with a
filtered iterator which might be used to extend this `iTree` too.
:type items: Iterable
:param items: iterable-object that contains `iTree`-objects as items it can be:
* iterator or generator of `iTree`-objects (using next)
* `iTree`-object (children will be copied and extended in this tree)
* iterable of `iTree`-objects (list, tuple, ...)
* argument list for `iTree`-instance ( ´__init__()´ ) (created by ´get_init_args()´
or ´get_init_args_deep()´ ) -> this is most often an internal functionality.
* iterator or generator of value-objects (using next) - implicit `iTree`-objects created
* iterable of value-objects (list, tuple, ...)- implicit `iTree`-objects created
"""
if self._flags & self._IS_TREE_PROTECTED:
if self.is_link_root:
# extend is allowed on link_root items
# we must check the items in this case!
items, check_items = tee(items, 2) # we must reuse the iterator in this case
if self._link.is_loaded:
flag_mask = self._LINKED | self._LINK_ROOT | self._PLACEHOLDER
error = any(
hasattr(i, '_itree_prt_idx') and i.flags & flag_mask
for i in check_items
)
if error:
raise PermissionError(
'It is not allowed to append linked items in an already linked item iTree')
else:
self._raise_read_only_exception(self)
return self._items.extend(_iTreePrivate._iter_extend(self, items))
[docs] def extendleft(self, items):
"""
Multy item append on left hand-side (at the beginning) of the ´iTree´.
The operation is slower than ´extend()´ because it requires a reordering of all items in the `iTree`.
.. note::
The order of extended items is kept in the operation. It's comparable with:
´[1,2,3]+[4,5,6]=[1,2,3,4,5,6]´ but the result is not a new instance, self is kept.
.. note:: In case the to be extended items have already a parent an implicit copy will be made.
We do this because the internal copy can be created more effective.
We accept also iTree-objects as extend_items parameter and the children which have a parent will be
automatically copied to be integrated in this second tree. We have the same situation with a
filtered iterator which might be used to extend this `iTree` too.
:type items: Iterable
:param items: iterable-object that contains `iTree`-objects as items it can be:
* iterator or generator of `iTree`-objects (using next)
* `iTree`-object (children will be copied and extended in this tree
* iterable of `iTree`-objects (list, tuple, ...)
* argument list for `iTree`-instance ( ´__init__()´ ) (created by ´get_init_args()´
or ´get_init_args_deep()´ )
* iterator or generator of value-objects (using next) - implicit `iTree`-objects created
* iterable of value-objects (list, tuple, ...)- implicit `iTree`-objects created
"""
if self._flags & self._IS_TREE_PROTECTED:
if self.is_link_root:
# extend is allowed on link_root items
# we must check the items in this case!
items, check_items = tee(items, 2) # we must reuse the iterator in this case
if self._link.is_loaded:
error = False
tags = self._link.tags
flag_mask = self._LINKED | self._LINK_ROOT | self._PLACEHOLDER
for i in check_items:
if hasattr(i, '_itree_prt_idx'):
if i.flags & flag_mask or i.tag in tags:
error = True
break
elif NoTag in tags:
error = True
break
if error:
self._raise_read_only_exception(self)
else:
self._raise_read_only_exception(self)
# prepare a list of the items in the tree
old_items = list(self._items.__iter__())
for i in old_items:
# delete parent so that no copy is required for later re-extend
i._itree_prt_idx = None
# clean the internal structure
self._families = {}
self._items.clear()
# extend new and old itmes
return self._items.extend(_iTreePrivate._iter_extend(self, chain(items, old_items)))
# item manipulations
[docs] def __setitem__(self, target, value):
"""
Replace an item with the given new item given in the `value`-parameter. The method handles also
multiple replaces (rearrangements) like:
::
>>> mytree[1],mytree[0]=mytree[0],mytree[1]
.. warning::
Because of the parent only principle in rearrangements operations
an implicit copy might be created.
.. note:: Linked items cannot be changed. If changes are required The user
must change the link source tree items
and afterwards actively rerun `load_links()` to reload the linked tree.
:except: In case the target is not found or the `iTree` is protected (read-only tree).
:param target: target object defining the replacement target;
possible types are:
* index - absolute target index integer (fastest operation)
* key - key tuple (family_tag, family_index)
* tag - Tag(family_tag) object targeting a whole family
* target-list - absolute indexes or keys to be replaced (indexes and keys can be mixed)
* index slice - slice of absolute indexes
* key slice - tuple: (family_tag, family_index_slice)
For multi targets the given value must have a matching structure (item list with same length).
We have two special targets which are used for placing/replacing single items in the iTree:
* Ellipsis `...` - new_items tag-family will be deleted and the new-item is placed in families
first item position
* items_tag - new_items tag-family will be delted and the new-item is placed in families
last item position
If those two special targets are used and the new-items family does not exist yet, the method will
just append the new item, no exception will be raised.
:param value: iTree object that should replace the target or in case of multi targets a
tuple of items that should be used for replacements
:return: value added items (only for internal usage)
"""
flags = self._flags
if flags & self._IS_TREE_PROTECTED:
if self.is_link_root:
# if self is link_root and the tag of the given item is different then the linked
# ones operation is allowed!
if hasattr(value, '_itree_prt_idx'):
if value.flags & (self._LINKED | self._LINK_ROOT | self._PLACEHOLDER):
raise TypeError('Linked items cannot be appended to linked item as local item')
if self._link.is_loaded and value.tag in self._link._tags:
self._raise_read_only_exception(self)
elif self._link.is_loaded and NoTag in self._link._tags:
self._raise_read_only_exception(self)
else:
self._raise_read_only_exception(self)
old_item = self.__getitem__(target)
if old_item is value:
return value
# rearrangement?
t = type(value)
if type(value) is tuple or type(value) is list or type(value) is itree_list:
old_items = old_item
it_setitem = self.__setitem__
return [it_setitem(old_items[i].idx, new) for i, new in enumerate(value)]
# prepare new item
if hasattr(value, '_itree_prt_idx'):
parent_list = value._itree_prt_idx
if parent_list is not None:
if parent_list[0] == self:
# reorder operation!
value = self._iter_copy(value, iTree._get_args_skip_subtree)
else:
raise RecursionError('Given item has already a parent iTree!')
else:
# implicit iTree definition
value = iTree(value=value)
if target is ...: # Ellipsis is used for single append
if self:
tag = value.tag
if tag in self._families:
# delete family
self.__delitem__(tag)
return self.append(value)
elif target == value.tag: # family tag replaces family with the single item (same position as fist item in family
tag = value.tag
family= self._get_fam(tag)
if family is None:
return self.append(value)
else:
list(
self._raise_exception(AttributeError('Single operations on linked items are not supported')) for
i
in family if i.is_link_cover)
old_item_idx = family[0].idx
sl = self._items
for i in reversed(family[1:]):
sl.__delitem__(i.idx)
sl.__setitem__(old_item_idx, value)
self._families[tag] = [value]
value._itree_prt_idx = (self, old_item_idx, 0)
return value
# normal setitem replaces old item
# handle old item
try:
abs_idx = old_item.idx
except AttributeError as e:
raise LookupError(
'Given target is not unique; set operation can only be made on unique items!'
) from e
# if old_item.is_linked:
# raise PermissionError('The target item is read_only (linked)!')
o_tag, o_fm_idx = old_item.tag_idx
# start the manipulation
old_item._itree_prt_idx = None
# replace old item in super list
self._items.__setitem__(abs_idx, value)
v_tag = value._tag
if v_tag == o_tag: # same family
family = self._getitem_fam(v_tag)
# replace old item in family list
family.__setitem__(o_fm_idx, value)
fm_idx = o_fm_idx
else:
# different families
# del old item from family
self._getitem_fam(o_tag).__delitem__(o_fm_idx)
try:
family = self._getitem_fam(v_tag)
fm_idx = self._get_family_insertion_idx(family, abs_idx)
family.insert(fm_idx, value)
except (KeyError, IndexError):
self._setitem_fam(v_tag, [value])
fm_idx = 0
value._itree_prt_idx = [self, abs_idx, fm_idx]
return value
[docs] def move(self, target=None):
"""
Move this item in given target position (item will be positioned **before** the given target).
The given target must be a unique item! If None is given
the item will be moved in the last position of the `iTree`. If an ìTree`-object
is given as target it must be a children of the same parent (sibling).
:type target: Union[Integer,tuple,iTree,None]
:except: LookupError in case the target is not found or not unique!
:param target: target-object defining the replacement target;
possible types are:
* index - absolute target index integer, negative values supported too (count from the end).
* key - key-tuple (family_tag, family_index) pair
* item - `iTree`-item that is already a children (future successor)
* None - if `None` is given we will move the item to the last position in the ´iTree´-object
:return: self (with updated indexes)
"""
if self._itree_prt_idx is None:
raise LookupError('This item is not a children of a iTree!')
parent = self._itree_prt_idx[0]
flags = parent._flags
if flags & self._IS_TREE_PROTECTED:
if parent.is_link_root:
if parent._link.is_loaded and self.tag in parent._link._tags:
self._raise_read_only_exception(self)
else:
self._raise_read_only_exception(self)
if target is None:
move_item = parent.__delitem__(self.idx)
return parent.append(move_item)
# check if target exists:
if type(target) is not int:
try:
target_idx = parent.__getitem__(target).idx
except AttributeError as e:
raise LookupError('Given target is not unique') from e
else:
target_idx = target
src_idx = self.idx
move_item = parent.__delitem__(src_idx)
return parent.insert(target_idx, move_item)
[docs] def rename(self, new_tag):
"""
give the item a new family tag
The renaming of the item implies a reordering of the items in the tree because the family order
depends on the global/absolute order of items.
:type new_tag: Hashable
:param new_tag: new tag (any kind of hashable object)
:rtype: iTree
:return: Delivers the renamed item itself
(it might be useful for the user to get the updated information of the object).
"""
parent_list = self._itree_prt_idx
if parent_list is not None:
parent = parent_list[0]
flags = parent._flags
if flags & self._IS_TREE_PROTECTED:
if parent.is_link_root:
if parent._link.is_loaded:
tags = parent._link._tags
if new_tag in tags:
self._raise_read_only_exception(self)
if self._flags & (self._LINKED | self._PLACEHOLDER):
self._raise_read_only_exception(self)
else:
self._raise_read_only_exception(self)
else:
self._tag = new_tag
return
families = parent._families
tag = self._tag
# remove old tag in the map-dict
family = families.__getitem__(tag)
if len(family) == 1:
families.__delitem__(tag)
else:
family.remove(self)
# insert new tag
self._tag = new_tag
if new_tag in families:
new_family = families.__getitem__(new_tag)
fm_idx = self._get_family_insertion_idx(new_family, self.idx)
new_family.insert(fm_idx, self)
self._itree_prt_idx[2] = fm_idx
else:
# create new family
families.__setitem__(new_tag, [self])
self._itree_prt_idx[2] = 0
return self
[docs] def reverse(self):
"""
Reverse the order of all children in the `iTree`.
If you do not want to change the object itself (in place operation) you might use the iterator
`reversed()` instead.
"""
flags = self._flags
if flags & self._IS_TREE_PROTECTED:
if not self.is_link_root or self._link.is_loaded:
self._raise_read_only_exception(self)
else:
self._items.reverse()
for family in self._families.values():
family.reverse()
[docs] def rotate(self, n=1):
"""
Rotate children of the `iTree`-object n times (n positions)
(rotate 1 times means move last item to first position)
If no parameter is given we rotate by one position only.
The rotation can be made in negative direction too (give negative numbers).
In case zero is given the operation is neutral and nothing will be changed.
.. note:: There is no in-depth counterpart of this method available.
:type n: integer
:param n: number of positions the items should be rotated
"""
flags = self._flags
if flags & self._IS_TREE_PROTECTED:
if not self.is_link_root or self._link.is_loaded:
self._raise_read_only_exception(self)
elif n > 0:
move_items = [self.pop() for _ in range(n)]
move_items.reverse()
self.extendleft(move_items)
elif n < 0:
move_items = [self.pop(0) for _ in range(-n)]
# move_items.reverse()
self.extend(move_items)
[docs] def sort(self, key=None, reverse=False):
"""
Sorting operation -> same behavior as sort of lists (parameter description is taken from list documentation).
.. note:: This is an "in place" operation which changes the content of the object the build-in `sorted()`
might be use instead (if the original object should not be changed):
>>> a=iTree(subtree=[iTree(3),iTree(2),iTree(4),iTree(1)])
>>> a.render()
iTree()
> iTree(3)
> iTree(2)
> iTree(4)
> iTree(1)
>>> b=iTree(subtree=(a[i] for i in sorted(a.keys())))
iTree()
> iTree(1)
> iTree(2)
> iTree(3)
> iTree(4)
Internally in this operation a copied sorted list is created, and afterwards the whole structure is cleared
and rebuild based on the sorted list.
The default-operation is to the sort based on the list of keys
(tag-family, family_index) pair of the items. The base of the sorting can be modified by changing
the `target_type` parameter.
:param key: specifies a function of one argument that is used to extract a comparison key
from each list element (for example, key=str.lower). The key corresponding to each item in
the list is calculated once and then used for the entire sorting process.
The default value of None means that list items are sorted directly without calculating
a separate key value.
:param reverse: is a boolean value. If set to True, then the list elements are sorted
as if each comparison were reversed.
"""
flags = self._flags
if flags & self._IS_TREE_PROTECTED:
if not self.is_link_root or self._link.is_loaded:
self._raise_read_only_exception(self)
else:
sort_list = list(self._items.__iter__())
sort_list.sort(key=key, reverse=reverse)
self._items.clear()
self._families = {}
self.extend(sort_list)
[docs] def __delitem__(self, target):
"""
The function deletes the targeted item in the tree.
:except: In case the target is not found or the `iTree` is protected (read-only tree).
:type target: Union[int,tuple,Hashable,Iterable,slice]
:param target: target object defining the replacement target;
possible types are:
* *index* - absolute target index integer (fastest operation)
* *key* - key tuple (family_tag, family_index)
* *tag* - Tag(family_tag) object targeting a whole family
* *target-list* - absolute indexes or keys to be replaced (indexes and keys can be mixed)
* *index-slice* - slice of absolute indexes
* *key-slice* - tuple of (family_tag, family_index_slice)
* *itree_filter* - method (callable) for filtering the children of the object
:return: deleted item
"""
is_link_root = False
if self._flags & self._IS_TREE_PROTECTED:
if not self.is_link_root:
self._raise_read_only_exception(self)
elif self.is_link_loaded:
is_link_root = True
if self:
t=type(target)
if t is int: # special very quick access:
if is_link_root:
item = self.getitem_by_idx(target)
if item._flags & (self._LINKED | self._PLACEHOLDER):
self._raise_read_only_exception(self)
del_item = self._items.pop(target)
tag = del_item._tag
family = self._getitem_fam(tag)
size_fam = len(family)
if hasattr(del_item, '_link') and del_item._link._link_item is not None:
f_idx = del_item.tag_idx[1]
link_item = del_item._link._link_item
self._items.insert(target, link_item)
family[f_idx] = link_item
link_item._itree_prt_idx = [self, target, f_idx]
del_item._itree_prt_idx = None
return del_item
elif size_fam - 1:
# find family index
i = del_item._itree_prt_idx[2]
if i < size_fam and del_item is family[i]:
family.__delitem__(i)
else:
start = 0
for _ in family: # for is quicker as while
i = family.index(del_item, start)
if family[i] is del_item:
f_idx = i
break
start = i + 1
family.__delitem__(f_idx)
else:
self._families.__delitem__(tag)
del_item._itree_prt_idx = None
return del_item
elif t is slice: # special quick access:
items=self._items
del_items = items[target]
if is_link_root:
for item in del_items:
if item._flags & (self._LINKED | self._PLACEHOLDER):
self._raise_read_only_exception(self)
del items[target]
for idx,del_item in zip(range(target.start,target.stop),del_items):
tag = del_item._tag
family = self._getitem_fam(tag)
size_fam = len(family)
if hasattr(del_item, '_link') and del_item._link._link_item is not None:
f_idx = del_item.tag_idx[1]
link_item = del_item._link._link_item
self._items.insert(idx, link_item)
family[f_idx] = link_item
link_item._itree_prt_idx = [self, idx, f_idx]
del_item._itree_prt_idx = None
return del_item
elif size_fam - 1:
# find family index
i = del_item._itree_prt_idx[2]
if i < size_fam and del_item is family[i]:
family.__delitem__(i)
else:
start = 0
for _ in family: # for is quicker as while
i = family.index(del_item, start)
if family[i] is del_item:
f_idx = i
break
start = i + 1
family.__delitem__(f_idx)
del_item._itree_prt_idx = None
return del_items
else:
items = self.__getitem__(target)
if hasattr(items, '_itree_prt_idx'):
return self.__delitem__(items.idx)
else:
return [self.__delitem__(item.idx) for item in items]
raise KeyError('Given target %s not found in item %s' % (repr(target), str(self)))
[docs] def clear(self, keep_value=False, local_only=False):
"""
deletes all children
and the value!
All flags stay unchanged, except the load_links flag!
:type keep_value: bool
:param keep_value:
* True - value is not deleted
* False - value will be replaced with NoValue
:type local_only: bool
:param local_only:
* True - clear only the local items
* False - clear whole object (The object is reset to the no links loaded
state and locals are deleted)
"""
flags = self._flags
if flags & self._IS_TREE_PROTECTED and not self.is_link_root:
self._raise_read_only_exception(self)
if self.is_link_root:
if local_only:
for item in self._iter_locals_add_placeholders(self):
if item.is_placeholder:
continue
self.__delitem__(item.idx)
else:
self._link._loaded = False
self._unset_flags(self, self._LOAD_LINKS)
self._items.clear()
self._families = {}
else:
self._unset_flags(self, self._LOAD_LINKS)
self._items.clear()
self._families = {}
if not keep_value:
self._value = NoValue
[docs] def pop(self, target=-1):
"""
pop the item out of the tree, if no key is given the last item will be popped out
We do not have the method popleft because `pop(0)` does the same.
:type target: Union[int,tuple,Hashable,Iterable,slice,iTree]
:param target: target of popped item(s):
* *index* - absolute target index integer (fastest operation)
* *key* - key tuple (family_tag, family_index)
* *tag* - Tag(family_tag) object targeting a whole family
* *target-list* - absolute indexes or keys to be replaced (indexes and keys can be mixed)
* *index-slice* - slice of absolute indexes
* *key-slice* - tuple of (family_tag, family_index_slice)
* *itree_filter* - method (callable) for filtering the children of the object
:return: popped out item(s) (parent will be set to None). In case multiple items are
removed an iterator over the removed items is given.
"""
return self.__delitem__(target)
[docs] def remove(self, item):
"""
With remove the given target is a `iTree` child that should be removed.
The method is only in because we like to be compatible with lists interface but the pop method target
allows already to use a child as a target too.
:except: If given item is not a child of the parent or the ìTree`-objects tree is protected
:type item: Union[iTree,Iterable]
:param item: Child or iterable of children to be removed from the tree
:return: removed item(s) (parent will be set to None) - in case of multiple removes the
method delivers a list no iterator because anyway a list is created
"""
if hasattr(item, '_itree_prt_idx'):
if item._itree_prt_idx is not None and item._itree_prt_idx[0] is self:
return self.__delitem__(item.idx)
else:
raise ValueError('Given iTree object is not a child of this iTree-object')
try:
is_link_root = self.is_link_root and self.is_link_loaded
item_list = list(
item) # we consume the iterator here because we need it multiple times we used list to reverse later on
for i in item_list: # check if the items in the iterator are valid for the operation
try:
if i._itree_prt_idx[0] is not self:
raise AttributeError()
if is_link_root and (i.is_linked or i.is_placeholder):
self._raise_read_only_exception(self)
continue
except AttributeError as e:
raise ValueError(
'The object %r is not a child of this iTree-object'
% repr(i)
) from e
for i in reversed(item_list): # we see advantage for most cases if we remove in reversed order
self.__delitem__(i.idx)
return item_list
except (PermissionError, ValueError):
raise
except:
raise TypeError('As item parameter we expect an iTree child or a Iterable of children')
# *** getters: *****************************************************************************************************
[docs] def __getitem__(self, target):
"""
Main common get method for children (first level items).
In case the given targets is a absolute index or a key (tag,family-index) pair the method will
deliver a unique item back. This operation is prioritized over the other operations.
For all other targets the method will deliver a list with the targeted items as result.
In some cases an empty list might be delivered and no exception might be raised
(e.g. filter query delivers no match).
In case user likes to have other return-types he might check the other available get methods
( `get()`, `get.single()`, `get.iter()`) or he might also use the itertree helper method `getter_to_list()` to
convert any of the possible results into a list.
:except: In case of no match (even if a part is not matching (e.g. one index in an index-list) the
method will raise
a KeyError (no matching target given); IndexError (no matching index given) or
ValueError (no valid type of target given).
:type target: Union[int,tuple,list,slice]
:param target: target object targeting a child or multiple children in the ´iTree´.
Possible types are:
* *index* - absolute target index integer (fastest operation)
* *key* - key tuple (family_tag, family_index)
* *index-slice* - slice of absolute indexes
* *key-index-slice* - tuple of (family_tag, family_index_slice)
* *target-list* - absolute indexes or keys to be replaced (indexes and keys can be mixed)
* *key-index-list* - tuple of (family_tag, family_index_list)
* *tag* - family_tag object targeting a whole family
* *tag-set* - a set of family-tags targeting the items of multiple families
* *itree_filter* - method (callable) for filtering the children of the object
* *all-children* - if build-in `iter` or `...`(Ellipsis) is given a list
of all children will be given (same like list(itree.__iter__())`)
:rtype: Union[iTree,list]
:return: Target was *index* or *key* -> one `iTree` item will be given;
for all other targets a list will be delivered.
"""
if self:
t = type(target)
if t is tuple:
try:
# check for key as quick as possible!
fam_tag, fam_idx = target # unpack to be sure we have a tuple of two items
if fam_idx is ...:
return self._getitem_fam(fam_tag)[:]
# key, family-index or key, family-index-slice:
return self._getitem_fam(fam_tag)[fam_idx]
except TypeError:
try:
family = self._getitem_fam(fam_tag)
return [family[i] for i in fam_idx]
except:
try:
return list(self._getitem_fam(target))
except:
raise ValueError('Given target {} is invalid'.format(repr(target))) # from e
except IndexError:
try:
return list(self._getitem_fam(target))
except:
raise IndexError(
'Given family-idx of target {} not found in iTree'.format(repr(target))) # from e
except:
try:
return list(self._getitem_fam(target))
except:
if 'fam_idx' in locals():
raise KeyError(
'Given target {} invalid or not found in iTree'.format(repr(target))) # from e
else:
raise ValueError('Given target {} is invalid'.format(repr(target))) # from e
elif t is int or t is slice:
# absolute index or absolute index-slice
try:
return self.getitem_by_idx(target)
except IndexError:
try:
# Maybe we have a tag that matches?
return list(self._getitem_fam(target))
except:
raise IndexError(
'Given abs-idx in target {} is out of range'.format(repr(target))) # from e
elif t is TagIdx: # downward compatibility
fam_tag, fam_idx = target # unpack
return self._getitem_fam(fam_tag)[fam_idx]
elif t is set:
# tags-set
result = []
for tag in target:
result.extend(self._families[tag])
if result:
return result
raise KeyError('No matching item found')
elif t is list:
# multiple targets given they will be combined in one list
result = []
for sub_target in target:
r = self[sub_target]
if type(r) is list:
result.extend(r)
else:
result.append(r)
if result:
return result
raise KeyError('No matching item found')
elif target is Ellipsis:
return self.getitem_by_idx(self._NoneSlice) # full slice is incredible fast on blists
elif callable(target):
if target is iter:
# give all items
return self.getitem_by_idx(self._NoneSlice)
# filter given?
try:
return list(filter(target, self))
except Exception:
if "<lambda>" in str(target):
# We try to identify in this case which child made the troubles
for c in self:
try:
target(c)
except Exception:
raise TypeError('lambda: raised an exception in filter-calculation, the %i. child %s'
' is incompatible with the calculation' % (c.idx, str(c)))
result= self._get_fam(target)
if result is not None:
return result[:] # slice is quicker then copy
raise KeyError('Given target: %s not found in iTree!' % repr(target))
# *** math operations and operations creating new/copied representations *******************************************
def __reversed__(self):
return self.__class__(self._tag,
value=copy.copy(self._value),
subtree=[i.__copy__() for i in reversed(list(self._items.__iter__()))],
# here we create a recursion -> subtree is copied!!
)
def __mul__(self, factor):
"""
Multiplication function a iTree is multiplied (copies) and put in a new iTree:
my_single_item=iTree('multi')
multi=my_single_item*1000
In case factor is another iTree the cartesian product will be calculated. The resulting iTree
will have a length of: len(self)*len(factor)*2 (The factor 2 results
from the difference of the cartesian to the normal product)
The subtree looks like (item1_x from self item2_x from factor):
item1_0, item_2_0, item1_0, item_2_1, item1_0, item2_2, ..., item1_1, item2_0, item1_1, item2_1, ...
HINT: In this operation multiple copies of the original item generated.
:param factor: integer to multiply with
:return: iTree object containing multiplied children
"""
if hasattr(factor, '_itree_prt_idx'):
if self.is_link_root:
raise TypeError('__mul__() on link-root items is not supported')
subtree = chain.from_iterable(
product((i.copy() for i in self), (i.copy() for i in factor)))
else:
subtree = repeat(self.copy(), factor)
return iTree(self._tag, copy.copy(self._value), subtree, None, self._flags)
def __rmul__(self, other):
if hasattr(other, '_itree_prt_idx'):
subtree = chain.from_iterable(
product((i.copy() for i in self), (i.copy() for i in other)))
else:
subtree = repeat(self.copy(), other)
return iTree(NoTag, NoValue, subtree)
def __sub__(self, other):
"""
To subtract two iTree objects we copy the self-object and we iterate over the other object items.
In case a matching key is found in self the item will be deleted in the copy.
:param other:
:return:
"""
new = self.copy()
if self.tag == other.tag:
new._tag = NoTag
else:
try:
new._tag = new._tag - other._tag
except Exception:
t = type(new._tag)
t2 = type(other._tag)
if t == t2:
if t is str:
new._tag = new._tag.replace(other._tag, '')
if t is bytes:
new._tag = new._tag.replace(other._tag, b'')
if self._value_equal(self.value, other.value):
new._value = NoValue
else:
try:
new._value = new._value - other._value
except Exception:
t = type(new._value)
t2 = type(other._value)
if t == t2:
if t is str:
new._value = new._value.replace(other._value, '')
if t is bytes:
new._value = new._value.replace(other._value, b'')
# This code might be improved!
del_idx = []
for item in other:
key = item.tag_idx
if key in new and new[key] == item:
del_idx.append(new[key].idx)
continue
new[key] = new[key] - item
for i in reversed(del_idx): # we delete from the end if not the index would change after deleting first one
del new[i]
return new
def __add__(self, other):
"""
If two iTree objects are added the children in the two added iTrees are copied and combined
to a new iTree object the other attributes are taken over from the first iTree in the given sum.
:param other: iTree object that should be added
:return: New iTree object containing copies of all children
"""
if hasattr(other, '_itree_prt_idx'):
if self.is_link_root:
raise TypeError('__add__() on link-root items is not supported')
return iTree(copy.copy(self._tag), copy.copy(self._value),
subtree=chain((item.__copy__() for item in self),
(item.__copy__() for item in other)), flags=self._flags)
else:
raise TypeError('Added item is not of type iTree')
def __copy__(self):
"""
create a copy of this item
The difference in between copy and deepcopy for iTree is just that we do in deepcopy a copy
of all data items too. In copy we just copy the iTData object not the items itself, they stay as pointers
to the original objects.
The operation is very important for `iTree`-class because of the one parent only principle we are forced to
do a copy of all sub-items (in-depth). It's not possible to copy just the top-level element only.
The function is used internally in extend operations too. And we can see (profiler) that
improvements in this method have big impact.
:return: copied iTree object
"""
return self._iter_copy(self, iTree._get_copy_args)
[docs] def copy_keep_value(self):
"""
Create a copy of this item.
The difference in between normal `copy()` and this method is that the value objects are
completely untouched in this operation (for immutable objects there is no difference
in between the two copy operations).
:return: copied iTree object
"""
return self._iter_copy(self, iTree._get_args_skip_subtree)
[docs] def copy(self, *args, **kwargs):
"""
create a copy of this item
The difference in between `copy()` and `deepcopy()` for `iTree` is just that we do in `deepcopy()` a deepcopy
of all value items. In `copy()` we just copy the value object not the items inside, the pointers
to the original objects are kept (for immutable objects there is no difference).
:return: copied iTree object
"""
return self._iter_copy(self, iTree._get_copy_args)
def __deepcopy__(self, *args, **kwargs):
"""
create a deepcopy of this item
The difference in between `copy()` and `deepcopy()` for `iTree` is just that we do in `deepcopy()` a deepcopy
of all value items. In `copy()` we just copy the value object not the items inside, the pointers
to the original objects are kept (for immutable objects there is no difference).
:return: deep copied new iTree object
"""
return self._iter_copy(self, iTree._get_deepcopy_args)
[docs] def deepcopy(self, *args, **kwargs):
"""
create a deepcopy of this item
The difference in between `copy()` and `deepcopy()` for `iTree` is just that we do in `deepcopy()` a deepcopy
of all value items. In `copy()` we just copy the value object not the items inside, the pointers
to the original objects are kept (for immutable objects there is no difference).
:return: deep copied new iTree object
"""
return self._iter_copy(self, iTree._get_deepcopy_args)
# *** size & comparisons *******************************************************************************************
# __len__() of the super-class blist is not overloaded!
[docs] def filtered_len(self, filter_method):
"""
Calculates the number of filtered children.
:type filter_method: Callable
:param filter_method: filter method that checks for matching items
and delivers `True`/`False`.
The filter_method targets always the `iTree`-child-object and checks a characteristic
of this object for matches (see :ref:`filter_method <filter_method>`)
:rtype: int
:return: Number of matching items found
"""
return sum(1 for _ in filter(filter_method, self))
def __contains__(self, target):
"""
Checks if an ´iTree´ object is part of the ´iTree´
for comparison == -> ´__eq__()´ is used. For finding a specific object use ´is_parent()´ or 'is_in()` instead.
In case no ´iTree´ object is given the function uses ´__getitem__´ to check
if matching item(s) exists.
.. note:: There is no coresponding in-depth function available the user can easy search via:
>>> # Let itree be the iTree object the target should be searched in
>>> any(tag == i.tag for i in itree.deep)
>>> any(searchkey == i[0][-1] for i in itree.deep.tag_idx_paths())
>>> s=len(index_list)
>>> any(len(i[0])>s and index_list == i[0][(-s+1):] for i in itree.deep.idx_paths())
:param target: iTree object searched for or a target used by ´__getitem__()´ method
:return:
* True - matching child is found
* False - no matching item found
"""
if hasattr(target, '_itree_prt_idx'):
for child in self:
if child == target:
return True
else:
with suppress(Exception):
item = self.__getitem__(target)
if not hasattr(item, '_itree_prt_idx'):
next(item)
return True
return False
[docs] def is_tag_in(self, tag):
"""
Checks if a iTree contains the given family-tag (first-level only)
:param tag: family tag
:return: True/False
"""
if self:
return tag in self._families
else:
return False
[docs] def is_in(self, item):
"""
Checks if the given object is child of the iTree.
Different to ´__contains__()´ we check here for the instance (specific) object (is)
and not based on ´__eq__()´.
:param item: iTree object to be searched for
:return:
* True - matching child is found
* False - no matching item found
"""
if hasattr(item, '_itree_prt_idx'):
p = item._itree_prt_idx
return p is not None and p[0] is self
else:
raise TypeError('Given item is not of type iTree')
[docs] def __eq__(self, other):
"""
compares if the tag, value and children content of another item matches with this item
.. note::
If you like to check if it is really the same object you should use ´is´ instead of ´==´ operator
:param other: other iTree
:return: boolean match result (True match/False no match)
"""
if self is other:
return True
try:
if self._tag != other._tag or \
len(self) != len(other) or \
not self._value_equal(self._value, other._value):
return False
for i, ii in zip(self.deep, other.deep):
try:
a1 = i.get_init_args(None, False)
a2 = ii.get_init_args(None, False)
if a1 == a2:
# quick compare might fail because of the value
continue
if self._value_equal(a1[1], a2[1]):
continue
return False
except AttributeError:
return False # None
except Exception:
if not self._value_equal(a1[1], a2[1]):
return False
elif a1[0] != a2[0]:
return False
elif len(a1) > 2 and a1[2:] != a2[2:]:
return False
return True
except Exception:
return False
def __ne__(self, other):
"""
This is just the inverse operation of __eq__
:param other: other item to be compared with
:return: True/False
"""
return not self.__eq__(other)
def __lt__(self, other):
"""
less than is a size comparison (length are compared)
:param other: iTree object self should be compared with
:return: True/False
"""
return len(self) < len(other)
def __le__(self, other):
"""
less than or equal is a size comparison (length are compared)
:param other: iTree object self should be compared with
:return: True/False
"""
return len(self) <= len(other)
def __gt__(self, other):
"""
greater than is a size comparison (length are compared)
:param other: iTree object self should be compared with
:return: True/False
"""
return len(self) > len(other)
def __ge__(self, other):
"""
greater than or equal is a size comparison (length are compared)
:param other: iTree object self should be compared with
:return: True/False
"""
return len(self) >= len(other)
[docs] def equal(self, other, check_coupled=False, check_flags=False):
"""
compares if the data content of another item matches with this item
:param other: other iTree
:param check_coupled: check the couple object too? (Default False)
:param check_flags: check the flags of the objects? (Default False)
:return: boolean match result (True match/False no match)
"""
if self is other:
return True
if check_flags:
check1 = lambda i, ii: i._flags == ii._flags
else:
check1 = lambda i, ii: True
if check_coupled:
check2 = lambda i, ii: (hasattr(i, '_coupled') and hasattr(ii, '_coupled')) and \
i._coupled is ii._coupled if (hasattr(i, '_coupled') or hasattr(ii, '_coupled')) \
else True
else:
check2 = lambda i, ii: True
check = lambda i, ii: check1(i, ii) and check2(i, ii)
if not check(self, other):
return False
try:
if self._tag != other._tag or \
len(self) != len(other) or \
not self._value_equal(self._value, other._value):
return False
for i, ii in zip(self.deep, other.deep):
if not check(i, ii):
return False
a1 = i.get_init_args(None, False)
a2 = ii.get_init_args(None, False)
try:
if a1 == a2:
# quick compare might fail because of the value
continue
if self._value_equal(a1[1], a2[1]):
continue
return False
except Exception:
if not self._value_equal(a1[1], a2[1]):
return False
elif a1[0] != a2[0]:
return False
elif len(a1) > 2 and a1[2:] != a2[2:]:
return False
return True
except Exception:
return False
[docs] def count(self, item):
"""
Counts how many equal (`==`) children are in the `iTree`-object.
:type item: iTree
:param item: The `iTree`-items will be compared with this item
:rtype: int
:return: Number of matching items found
"""
return sum(item == i for i in self)
[docs] def index(self, item, start=None, stop=None):
"""
The index method allows to search for the absolute index of a matching item in the `iTree`.
The item must be a iTree object and the index will deliver the first match. The comparison is made via
`==` operator.
If item is not found a IndexError will be raised
.. note:: To get the index of a specific item instance the `.idx`- property
should be used.
:type item: iTree
:param item: iTree object to be searched for
:type start: Union[iTree,target_path]
:param start: iTree item or start target_path where index search should be started (start item is included in search)
:type stop: Union[iTree,target_path]
:param stop: iTree item or stop target_path where index search should be stopped (stop item is not included in search)
;rtype: int
:return: absolute index of the found item
"""
if self:
iterator = self.__iter__()
if start is not None:
if not hasattr(start, '_itree_prt_idx'):
start = self.get.single(start)
try:
first_item = next(dropwhile(lambda i: i is not start, iterator))
if first_item == item:
return first_item.idx
except StopIteration:
raise IndexError('No matching item found in iTree')
if stop is not None:
if not hasattr(stop, '_itree_prt_idx'):
stop = self.get.single(stop)
try:
item = next(dropwhile(lambda i: i is not stop and i != item, iterator))
if item is not stop:
return item.idx
raise StopIteration
except StopIteration:
raise IndexError('No matching item found in iTree')
else:
try:
item = next(dropwhile(lambda i: i != item, iterator))
return item.idx
except StopIteration:
raise IndexError('No matching item found in iTree')
def __hash__(self):
"""
The hash operation is available
.. node::As for the `==` operator we do not consider, parent, coupled items or flags properties of the object
:return: integer hash
"""
try:
h = hash(self._value)
except TypeError:
h = hash(pickle.dumps(self._value))
return hash((tuple(self), self._tag, h))
# *** ITERATORS ****************************************************************************************************
# standard iterators list/dict
# def __iter__(self): #use function of the super class!
# dict like iterators:
[docs] def keys(self, filter_method=None):
"""
Iterates over all children and deliver the children tag-idx tuple (family-tag,family_index)
.. note::
This is a dict like iterator that delivers the unique keys for all children.
:type filter_method: Union[Callable,None]
:param filter_method: filter method that checks the item
and delivers `True`/`False`.
The filter_method targets always the `iTree`-child-object and checks a characteristic
of this object for matches
If `None` is given filtering is inactive.
:rtype: Iterator
:return: iterator over the tag-idx of the children
"""
tag_cnts = {tag: -1 for tag in self._families.keys()}
if filter_method:
for item in self:
tag = item._tag
# as side effect we update the item cache too:
item._itree_prt_idx[2] = tag_cnts[tag] = cnt = tag_cnts[tag] + 1
if filter_method(item):
yield tag, cnt
else:
for item in self:
tag = item._tag
item._itree_prt_idx[2] = tag_cnts[tag] = cnt = tag_cnts[tag] + 1
yield tag, cnt
[docs] def values(self, filter_method=None):
"""
Iterates over all children and deliver the children values
:type filter_method: Union[Callable,None]
:param filter_method: filter method that checks for matching items
and delivers `True`/`False`.
The filter_method targets always the `iTree`-child-object and checks a characteristic
of this object for matches (see :ref:`filter_method <filter_method>`)
If `None` is given filtering is inactive.
:rtype: Iterator
:return: iterator over the values stored in the children
"""
if filter_method:
for item in filter(filter_method, self):
yield item._value
else:
for item in self:
yield item.value
[docs] def items(self, filter_method=None, values_only=False):
"""
Iterates over all children and deliver the children
item-tuples (key,item) or (key,value). As key we use the unique tag-idx: (tag-family,family-index).
The function is comparable with dicts `items()` function.
:type filter_method: Union[Callable,None]
:param filter_method: filter method that checks for matching items
and delivers `True`/`False`.
The filter_method targets always the `iTree`-child-object and checks a characteristic
of this object for matches (see :ref:`filter_method <filter_method>`)
If `None` is given filtering is inactive.
:type values_only: bool
:param values_only:
* `False` (default) - in the key,value tuple the iterator put the iTree object as value in
* `True` - in the key,value tuple the iterator put "only" the value object of the `iTree`-object in
:rtype: Generator
:return: iterator over the target keys and item value of the children
"""
tag_cnts = {tag: -1 for tag in self._families.keys()}
if values_only:
if filter_method:
for item in self:
tag = item._tag
# as side effect we update the item cache too:
item._itree_prt_idx[2] = tag_cnts[tag] = cnt = tag_cnts[tag] + 1
if filter_method(item):
yield (tag, cnt), item.value
else:
for item in self:
tag = item._tag
item._itree_prt_idx[2] = tag_cnts[tag] = cnt = tag_cnts[tag] + 1
yield (tag, cnt), item.value
elif filter_method:
for item in self:
tag = item._tag
# as side effect we update the item cache too:
item._itree_prt_idx[2] = tag_cnts[tag] = cnt = tag_cnts[tag] + 1
if filter_method(item):
yield (tag, cnt), item
else:
for item in self:
tag = item._tag
item._itree_prt_idx[2] = tag_cnts[tag] = cnt = tag_cnts[tag] + 1
yield (tag, cnt), item
[docs] def iter_families(self, filter_method=None, order_last=False):
"""
This is a special iterator that iterates over the families in `iTree`. It delivers per family the tag and
a list of the containing items. The order is defined by the absolute index of the first item in each family
Method will be reached via `iTree.Families.iter()`
:type filter_method: Union[Callable,None]
:param filter_method: filter method that checks for matching items
and delivers `True`/`False`.
The filter_method targets always the `iTree`-child-object and checks a characteristic
of this object for matches (see :ref:`filter_method <filter_method>`)
If filter_method is None no filtering is performed
.. note:: An internal filtering is available because this may change the order of
the delivered items. An external filter with same method might
deliver a different result!
:type order_last: bool
:param order_last:
* False (default) - The tag-order is based on the order of the first items in the family
* True - The tag-order is based on the order of the last items in the family
:rtype: Generator
:return: iterator over all families delivers tuples of (family-tag, family-item-list)
"""
if self:
if order_last:
index = -1
else:
index = 0
if filter_method:
return ((i.tag, [i for i in self._getitem_fam(i._tag) if filter_method(i)])
for i in sorted((v[index] for v in self._families.values()), key=lambda i: i.idx))
else:
return ((i.tag, list(self._getitem_fam(i._tag)))
for i in sorted((v[index] for v in self._families.values()), key=lambda i: i.idx))
[docs] def iter_family_items(self, order_last=False):
"""
This is a special iterator that iterates over the families in `iTree`. It iters over the items of each family
the ordered by the first or the last items of the families.
:type order_last: bool
:param order_last:
* False (default) - The tag-order is based on the order of the first items in the family
* True - The tag-order is based on the order of the last items in the family
:rtype: Generator
:return: iterator over all families delivers tuples of (family-tag, family-item-list)
"""
if self:
if order_last:
index = -1
else:
index = 0
for i in sorted((v[index] for v in self._families.values()), key=lambda i: i.idx):
for item in self._getitem_fam(i._tag):
yield item
# *** outputs/dumps ************************************************************************************************
def __repr__(self):
"""
Create representation string from which the object can be theoretically be reconstructed via `eval()`
(might not work in case of
value-objects that do not have a working `__repr()` method)
:rtype: str
:return: representation string
"""
out = ['iTree(']
if self._tag is not NoTag:
out.append(repr(self._tag))
out.append(', ')
if self._value is not NoValue:
out.append('value=')
out.append(repr(self._value))
out.append(', ')
if self:
if self.level > (sys.getrecursionlimit() / 5):
out.append('subtree=[ ... ]')
else:
subtree = self._items.__repr__()
if subtree[0] == 'b':
# we shorten blist from definition
subtree = subtree[6:-1]
out.append('subtree=')
out.append(subtree)
out.append(', ')
is_links_loaded = False
if hasattr(self, '_link'):
link = self._link
if link._link_item is not None:
out.append('link=iTLink(link_item=%s)' % (repr(link._link_item)))
else:
out.append('link=iTLink(%s,%s)' % (repr(link.file_path), repr(link.target_path)))
is_links_loaded = link.is_loaded
out.append(', ')
if self._flags or is_links_loaded:
flags = self._flags
if is_links_loaded:
flags = flags | iTFLAG.LOAD_LINKS
out.append('flags=%s' % (bin(flags)))
if out[-1] == ', ':
out = out[:-1]
if out[-1] == ',':
out = out[:-1]
out.append(')')
return ''.join(out)
def __str__(self):
"""
String repr of the item stripping the subtree to the first and last element only and giving ".." inbetween
For full representation-string use `repr()`.
:return: shorten representation string
"""
out = ['iTree(']
if self._tag is not NoTag:
out.append(repr(self._tag))
out.append(', ')
if self._value is not NoValue:
out.append('value=')
out.append(repr(self._value))
out.append(', ')
if len(self):
if self.level > (sys.getrecursionlimit() / 5):
out.append('subtree=[ ... ]')
else:
out.append('subtree=[')
if len(self) <= 2:
for i in self:
out.append(str(i))
out.append(',')
out[-1] = ']'
else:
out.append(str(self[0]))
out.append(',...,')
out.append(str(self[-1]))
out.append(']')
out.append(', ')
is_links_loaded = False
if hasattr(self, '_link'):
link = self._link
if link._link_item is not None:
out.append('link=iTLink(link_item=%s)' % (repr(link._link_item)))
else:
out.append('link=iTLink(%s,%s)' % (repr(link.file_path), repr(link.target_path)))
is_links_loaded = link.is_loaded
out.append(', ')
if self._flags or is_links_loaded:
flags = self._flags
if is_links_loaded:
flags = flags | iTFLAG.LOAD_LINKS
out.append('flags=%s' % (bin(flags)))
if out[-1] == ', ':
out = out[:-1]
if out[-1] == ',':
out = out[:-1]
out.append(')')
return ''.join(out)
[docs] def renders(self, filter_method=None, enumerate=None, renderer=iTreeRender):
"""
render the iTree into a string
:type filter_method: Union[Callable,None]
:param filter_method: filter method that checks for matching items
and delivers `True`/`False`.
The filter_method targets always the `iTree`-child-object and checks a characteristic
of this object for matches (see :ref:`filter_method <filter_method>`)
If `None` is given filtering is inactive.
The method uses the given filter always as an hierachical filter.
:type enumerate: bool
:param enumerate:
* True - Add an enumeration before the items
* False (default) - Output without enumeration
:type renderer: class
:param renderer: Give another renderer class for different formatting
:rtype: str
:return: Tree representation as string
"""
render_obj = getattr(self, '__renderer', None)
if render_obj is None or type(render_obj) != renderer:
self.__renderer = render_obj = renderer()
return render_obj.renders(self, filter_method, enumerate)
[docs] def render(self, filter_method=None, enumerate=False, renderer=iTreeRender):
"""
Print the rendered string of the `iTree`-object to the console (stdout).
:type filter_method: Union[Callable,None]
:param filter_method: filter method that checks for matching items
and delivers `True`/`False`.
The filter_method targets always the `iTree`-child-object and checks a characteristic
of this object for matches.
If `None` is given filtering is inactive.
:param enumerate: add an enumeration before the rendered items
:param renderer: Render to be used (The given render is stored and will be used until another renderer is given).
:return:
"""
print(
self.renders(filter_method, enumerate, renderer=iTreeRender).encode(
errors='replace').decode(
'utf8')[:-1])
# for pickle
def __reduce__(self):
return iTree, tuple(self.get_init_args())
[docs] def get_init_args(self, filter_method=None, _subtree_not_none=True):
"""
Method creates list of arguments that can be used as a pointer to create an equal instance of an iTree object.
This is a method is used in most cases for internal functionalities (especially copy()).
:type filter_method: Union[Callable,None]
:param filter_method: filter method that checks for matching items
and delivers `True`/`False`.
The filter_method targets always the `iTree`-child-object and checks a characteristic
of this object for matches (see :ref:`filter_method <filter_method>`)
If `None` is given filtering is inactive.
:param _subtree_not_none: internal parameter controlling if the subtree is added or not
:return:
"""
if _subtree_not_none and self:
if self.is_link_root:
if filter_method:
subtree = list(filter(filter_method, self._iter_locals_add_placeholders(self)))
else:
subtree = list(self._iter_locals_add_placeholders(self))
elif filter_method:
subtree = list(filter(filter_method, self))
else:
subtree = list(self)
if len(subtree) == 0:
subtree = None
else:
subtree = None
result = [self._tag, self._value, subtree]
if self.is_link_root:
result.append(iTLink(self._link.file_path, self._link.target_path))
flags = self._flags
if flags:
if len(result) == 3:
result.append(None)
result.append(flags)
return result
# serialize + file operations
[docs] def loads(self, data_str, check_hash=True, load_links=True,
itree_serializer=iTStdJSONSerializer2):
"""
create an iTree object by loading from a string
If not overloaded or reinitialized the iTree Standard Serializer will be used. In this case we expect a
matching JSON representation.
:param data_str: source string that contains the iTree information
:param check_hash: True the hash of the file will be checked and the loading will be stopped if it doesn't match
False - do not check the iTree hash
:param load_links: True - linked iTree objects will be loaded
:param itree_serializer: optional user defined serializer for iTree objects
:return: iTree object loaded from file
"""
serializer_obj = getattr(self, '__itree_serializer', None)
if serializer_obj is None or type(serializer_obj) != itree_serializer:
self.__itree_serializer = serializer_obj = itree_serializer(iTree)
return serializer_obj.loads(data_str, check_hash=check_hash, load_links=load_links)
[docs] def load(self, file_path, check_hash=True, load_links=True,
itree_serializer=iTStdJSONSerializer2):
"""
create an iTree object by loading from a file
If not overloaded or reinitialized the iTree Standard Serializer will be used. In this case we expect a
matching JSON representation.
:param file_path: file path to the file that contains the iTree information
:param check_hash: True the hash of the file will be checked and the loading will be stopped if it doesn't match
False - do not check the iTree hash
:param load_links: True - linked iTree objects will be loaded
:param itree_serializer: optional user defined serializer for iTree objects
:return: iTree object loaded from file
"""
serializer_obj = getattr(self, '__itree_serializer', None)
if serializer_obj is None or type(serializer_obj) != itree_serializer:
self.__itree_serializer = serializer_obj = itree_serializer(iTree)
return serializer_obj.load(file_path, check_hash=check_hash, load_links=load_links)
[docs] def dumps(self, calc_hash=False, filter_method=None,
itree_serializer=iTStdJSONSerializer2):
"""
serializes the iTree object to JSON (default serializer)
:param calc_hash: Tell if the hash should be calculated and stored in the header of string
:param itree_serializer: optional user defined serializer for iTree objects
:return: serialized string (JSON in case of default serializer)
"""
serializer_obj = getattr(self, '__itree_serializer', None)
if serializer_obj is None or type(serializer_obj) != itree_serializer:
self.__itree_serializer = serializer_obj = itree_serializer(iTree)
return serializer_obj.dumps(self, calc_hash=calc_hash, filter_method=filter_method)
[docs] def dump(self, target_path, pack=True, calc_hash=True, overwrite=False,
filter_method=None,
itree_serializer=iTStdJSONSerializer2):
"""
serializes the iTree object to JSON (default serializer) and store it in a file
:param target_path: target path of the file where the iTree should be stored in
:param pack: True - data will be packed via gzip before storage
:param calc_hash: True - create the hash information of iTree and store it in the header
:param overwrite: True - overwrite an existing file
:param itree_serializer: optional user defined serializer for iTree obbjects
:return: True if file is stored successful
"""
serializer_obj = getattr(self, '__itree_serializer', None)
if serializer_obj is None or type(serializer_obj) != itree_serializer:
self.__itree_serializer = serializer_obj = itree_serializer(iTree)
return serializer_obj.dump(self,
target_path,
pack=pack,
calc_hash=calc_hash,
overwrite=overwrite,
filter_method=filter_method)
# *** link root related functions: *********************************************************************************
# *** link related properties **************************************************************************************
@property
def is_placeholder(self):
"""
Property shows that item is a placeholder class
Normally there should be no placeholder class in the iTree but in case a loaded link does no more contain
the expected items it might happen that such a class artifact is still in the tree.
In placeholders the value contains the family index in the linked class.
:rtype: bool
:return: True/False
"""
return bool(self._flags & self._PLACEHOLDER)
@property
def is_link_cover(self):
"""
If the item is local and covers a linked item the property is True
:rtype: bool
:return: True/False
"""
return hasattr(self, '_link') and self._link and (type(self._link._link_item) is iTree)
@property
def is_linked(self):
"""
In contrast to iTreeLinked class this is False
:rtype: bool
:return: True/False
"""
return bool(self._flags & self._LINKED)
@property
def is_link_loaded(self):
if hasattr(self, '_link'):
return self._link.is_loaded
else:
# we return False in case we have no link_roots inside the subtree
return any(
i.is_link_loaded for i in filter(iTree._filter_linked_roots, self.deep))
@property
def link_root(self):
"""
delivers the highest level item that is linked
in case item is not linked it delivers itself
:rtype: iTree
:return: highest level linked item found in the parents
"""
if self.is_linked:
parent = self._itree_prt_idx
if (parent is not None) and parent[0].is_linked:
return parent[0].link_root
else:
return self
return None
@property
def is_link_root(self):
"""
property that marks the iTree item as an item that contains a link
:return:
* True - is a link root item
* False is no iTree link item
"""
return bool(self._flags & self._LINK_ROOT)
[docs] def load_links(self, force=False, delete_invalid_items=False, _items=None, _depth=0):
"""
Runs ove all children and sub children in case a ITreeLink object is found the linked items are load in
In case ´iTree´ is link root: load all linked items
:param force:
* False (default) - load only if not already loaded
* True - load even if already loaded (update)
:param delete_invalid_items:
* False (default) - in case of invalid items we will raise an exception!
* True - invalid items will be removed from parent no exception raised
:param _items: internal list parameter used for recursive calls of the function
:param _depth: Internal parameter related to current item depth
:return:
* True - success
* False - load failed
"""
if _depth > 200:
raise RecursionError('Circular link definition couldnot integrate linked item '
'%s' % (repr(self.tag_idx_path)))
if self.is_link_root:
load_ok = True
load_item = None
link_handler = self._link
if link_handler is not None:
if force:
load_active = True
else:
# We try to detect if the original link was changed
load_active = False
if link_handler.is_loaded:
load_active = link_handler.is_file_updated()
if not load_active and link_handler.file_path is None:
# internal link check the source tree
try:
target_tree = link_handler.get_target_tree(self)
if len(target_tree) != len(link_handler._keys):
raise Exception()
for key_old, key_new in zip(link_handler._keys,
target_tree.keys()):
if key_old != key_new:
raise ValueError()
except:
load_active = True
else:
load_active = True
if load_active:
try:
load_item = link_handler.get_target_tree(self)
except:
if delete_invalid_items:
if self._itree_prt_idx is not None:
self._itree_prt_idx[0].remove(self)
return False
else:
raise
# keep the locals and coupled objects then clean all
sl = self._items
# now we take over the tree
local_items = OrderedDict()
for i in self._iter_locals_add_placeholders(self):
if i.is_placeholder:
local_items[(i._tag, i._value)] = i
else:
local_items[i.tag_idx] = i
old_coupled_objects = {i.tag_idx: i.coupled_object for i in self if
i.is_linked and i.coupled_object}
sl.clear()
self._families = families = {}
linked_flag = self._LINKED
tags = set()
keys = []
append_item_to_tree = self._append_item
convert_to_linked_item = self._convert_to_linked_item
# load the linked items
for item in load_item:
if item.is_link_root:
try:
item.load_links(force=force, delete_invalid_items=delete_invalid_items,
_depth=(_depth + 1))
new_item = item.copy()
except (TypeError, RecursionError):
raise RecursionError('Circular link definition couldnot integrate linked item '
'%s' % (repr(item.tag_idx_path)))
new_item._flags = new_item._flags | linked_flag
else:
new_item = convert_to_linked_item(item)
key = item.tag_idx
if key in old_coupled_objects:
new_item.set_coupled_object(old_coupled_objects[key])
keys.append(key)
tags.add(key[0])
# here we build the order of locals ("inheritance" of structure)
if key in local_items:
# append all locals before the match
append_list = []
for k, i in local_items.items():
if k == key:
if i.is_placeholder:
append_list.append((k, new_item))
else:
i._link = iTLink(link_item=new_item)
append_list.append((k, i))
break
elif k in load_item:
append_list = []
elif not i.is_placeholder:
# in case of placeholders the new linked tree is reordered
# and we skip the old placeholders
# the idea is to keep at least the order of the local items
append_list.append((k, i))
# this does not work must investigate why
# sl.extend(
# self._iter_extend(self, ((local_items.__delitem__(k ) or i) for k,i in append_list)))
for k, i in append_list:
del local_items[k]
append_item_to_tree(self, i)
else:
append_item_to_tree(self, new_item)
# append all other local_items
# we do not use _iter_extend() here to avoid copies
for i in filter(lambda i: not i.is_placeholder, local_items.values()):
append_item_to_tree(self, i)
link_handler.set_tags_and_keys(tags, keys)
link_handler.set_loaded(load_item.tag, load_item.value)
return True
else:
return False
else:
loaded = False
for i in filter(iTree._filter_linked_roots, self.deep):
if i.load_links(force=force, delete_invalid_items=delete_invalid_items, _depth=(_depth + 1)):
loaded = True
return loaded
[docs] def make_local(self, copy_subtree=True):
"""
make the current linked object a local object
This is only possible if the parent is a iTree object is the link root->
only the first level children in a linked iTree can be made local
The operation raises an SyntaxError in case it is used on a deeper level of the linked tree
:return: None
"""
if self._itree_prt_idx is None or not self.is_linked:
raise SyntaxError('Item is not linked or has no parent, invalid operation')
parent = self._itree_prt_idx[0]
if not parent.is_link_root:
raise SyntaxError('local items can just be added to the root objects of links')
local_item = self._convert_to_local_item(self, copy_subtree)
abs_idx = self.idx
tag, f_idx = self.tag_idx
# replace old item in super list
parent._items[abs_idx] = local_item
# replace old item in family list
parent._getitem_fam(tag)[f_idx] = local_item
local_item._itree_prt_idx = [parent, abs_idx, f_idx]
return local_item
# *** unsupported methode (overload super() methods with exceptions ************************************************
def __isub__(self, other):
raise TypeError('iTree: unsupported operand or function')
def __imul__(self, other):
raise TypeError('iTree: unsupported operand or function')
# *** helpers ******************************************************************************************************
# property for debugging
@property
def _debug_children_list(self):
"""
This is a property for debugging proposes only
:return: list of children
"""
return list(self)