Source code for itertree.itree_main

# -*- 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
[docs] def tags(self, order_last=False): """ iters over all family-tags in level 1 (children). The order is based on first or last item in the family. :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: Iterator :return: tag iterator """ if self: s = len(self._families) s2 = len(self) if s2 == 1: # only a single family tag exists yield self.getitem_by_idx(0).tag elif s == s2: # all items in the tree have another tag for i in self: yield i.tag else: 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): yield i.tag
# *** 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 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)