"""
If the children attributes of a :class:`NodeThing <blueprints.thing.node.NodeThing>` are called, those children will be
returned in a :class:`View`. The :class:`View` can be used to get and set attributes of multiple objects
as well as to call methods on those child Things simultaneously. The following example shows how to build
a simplified hand model using Views.
.. code-block:: python
:caption: Getting attributes
>>> names = ['thumb', 'index', 'middle', 'ring', 'pinky']
>>> fingers = [blue.Body(name=name) for name in names]
>>> palm = blue.Body(name='palm', bodies=fingers)
>>> view = palm.bodies # calling child attributes creates a view
>>> view
View[5|palm.bodies]
>>> view.name
['thumb', 'index', 'middle', 'ring', 'pinky']
>>> view.bodies
View[0|palm.bodies.bodies]
>>> intermediate = blue.Body(name='intermediate')
>>> distal = blue.Body(name='distal')
>>> phalanges = [distal, intermediate, intermediate, intermediate, intermediate]
>>> view.attach.distribute(phalanges)
>>> view.bodies[1:].attach(blue.Body(name='distal'))
.. code-block:: python
:caption: Setting attributes
>>> view.bodies[0].name = 'distal_thumb'
>>> view.bodies[1:].name = ['intermediate_index',
'intermediate_middle',
'intermediate_ring',
'intermediate_pinky']
>>> view.bodies.name
['distal_thumb',
'intermediate_index',
'intermediate_middle',
'intermediate_ring',
'intermediate_pinky']
.. code-block::
:caption: Applying functions
>>> renaming = lambda x: x.__setattr__('name', f'distal_{x.parent.parent.name}')
>>> view.bodies.bodies.apply(renaming)
>>> view.bodies.bodies.name
['distal_index', 'distal_middle', 'distal_ring', 'distal_pinky']
.. code-block:: mxml
:caption: XML structure
<body name="palm">
<body name="thumb">
<body name="distal_thumb" />
</body>
<body name="index">
<body name="intermediate_index">
<body name="distal_index" />
</body>
</body>
<body name="middle">
<body name="intermediate_middle">
<body name="distal_middle" />
</body>
</body>
<body name="ring">
<body name="intermediate_ring">
<body name="distal_ring" />
</body>
</body>
<body name="pinky">
<body name="intermediate_pinky">
<body name="distal_pinky" />
</body>
</body>
</body>
"""
import numpy as np
import blueprints as blue
from collections import defaultdict, deque
from itertools import count, chain
from types import BuiltinFunctionType, FunctionType, MethodType
[docs]
class View(blue.ViewType):
"""
The View class is used to handle the access to children of nodes in a kinematic tree and their properties.
"""
__RESERVED = {'_View__ELEMENTS',
'_View__NAME',
'_View__PARENT',
'_View__RESERVED',
'_AllView__ELEMENTS',
'_AllView__NAME',
'_AllView__PARENT',
'_AllView__RESERVED',
'_AllView__DESCENDANTS'}
[docs]
@blue.restrict
def __init__(self, elements: list, name: str, parent: blue.ThingType|blue.LatticeType) -> None:
"""
The View can be used to get and set attributes of multiple objects at the same time.
Parameters
----------
elements : list
All Things, that are part of the View.
name : str
The name of the attribute that was requested for this View.
parent : blue.ThingType
The parent of the View.
"""
# ELIMINATE DUPLICATES (order-preserving, O(n))
elements = list(dict.fromkeys(elements))
self.__ELEMENTS = elements.copy()
self.__NAME = name
self.__PARENT = parent
def __repr__(self) -> str:
"""
Represents the View as a string.
Returns
-------
str
The representing string contains a representation of the call that was used to get the View.
"""
return f'View[{len(self)}|{self.__PARENT.name}.{self.__NAME}]'
def __len__(self):
"""
The number of Things contained in the View.
Returns
-------
int
The number of Things contained in the View.
"""
return len(self.__ELEMENTS)
def __iter__(self):
"""
Used to iterate over all Things in the View. While iterating over the View, its contents should not be modified.
Yields
------
ThingType
The next Thing in the list of elements.
"""
for element in self.__ELEMENTS:
yield element
def __bool__(self):
"""
Indicating whether the View contains elements.
Returns
-------
bool
If the View is empty the resulting value is False and True otherwise.
"""
return len(self.__ELEMENTS) > 0
@blue.restrict
def __getitem__(self, key: int|slice|str) -> blue.ThingType|blue.ViewType:
"""
This method either returns a single Thing from the View or a new View from a subset of the View.
Parameters
----------
key : int | slice | str
If the argument is an integer, the Thing with its corresponding index is returned.
If the argument is a slice, a View containing the Things according to the slices indicies is returned.
if the argument is a string, a View containing the Things with the same user defined names is returned.
Returns
-------
blue.ThingType | blue.ViewType
Raises
------
NotImplemented
If the argument is neither (int, slice, str) NotImplemented is raised.
"""
if isinstance(key, int):
return self.__ELEMENTS[key]
elif isinstance(key, slice):
return View(self.__ELEMENTS[key], self.__NAME, self.__PARENT)
elif isinstance(key, str):
return View(list(filter(lambda x: x._name == key, self)), self.__NAME, self.__PARENT)
else:
raise NotImplemented
def __getattr__(self, attr: str) -> list|blue.ViewType|blue.FunctionHandleType:
"""
This methods handles the access to the attributes of the Things contained in the View
depending on the type of attribute:
1. *property* In this case a list of all the singularly retrieved attributes is returned.
2. *children* This returns a new View, that contains the children of all the Views Things.
3. *method/function* In this case a FunctionHandle is returned.
In the last case the :class:`FunctionHandle` can either be called directly and all arguments are
passed to each of the Views Thing or the distribute method of the FunctionHandle can be used,
to match each argument to each Thing in a one to one relation.
.. code-block::
:caption: Example
>>> names = ['thumb', 'index', 'middle', 'ring', 'pinky']
>>> fingers = [blue.Body(name=name) for name in names]
>>> palm = blue.Body(name='palm', bodies=fingers)
>>> view = palm.bodies # calling child attributes creates a view
>>> view
View[5|palm.bodies]
>>> view.name # CASE 1
['thumb', 'index', 'middle', 'ring', 'pinky']
>>> view.bodies # CASE 2
View[0|palm.bodies.bodies]
>>> intermediate = blue.Body(name='intermediate')
>>> distal = blue.Body(name='distal')
>>> phalanges = [distal, intermediate, intermediate, intermediate, intermediate]
>>> view.attach.distribute(phalanges) # CASE 3
>>> view.bodies[1:].attach(blue.Body(name='distal')) # CASE 3
.. code-block:: mxml
:caption: XML structure of the previous example
<body name="palm">
<body name="thumb">
<body name="distal_(0)" />
</body>
<body name="index">
<body name="intermediate_(0)">
<body name="distal_(1)" />
</body>
</body>
<body name="middle">
<body name="intermediate_(1)">
<body name="distal_(2)" />
</body>
</body>
<body name="ring">
<body name="intermediate_(2)">
<body name="distal_(3)" />
</body>
</body>
<body name="pinky">
<body name="intermediate_(3)">
<body name="distal_(4)" />
</body>
</body>
</body>
Parameters
----------
attr : str
All attributes are valid, aslong as they are properties or methods of the Thing type contained in the View.
Returns
-------
list | blue.ViewType | blue.FunctionHandleType
Raises
------
AttributeError
If the attribute cannot be retrieved an Error is raised.
"""
if len(self) == 0:
raise AttributeError(f'{self} is empty, attribute "{attr}" cannot be retrieved.')
if all(hasattr(x, attr) for x in self):
elements = list(map(lambda x: getattr(x, attr), self))
if all(map(lambda x: isinstance(x, blue.ViewType), elements)):
elements = list((item for view in elements for item in view))
return View(elements, f'{self.__NAME}.{attr}', self.__PARENT)
elif all(map(lambda x: isinstance(x, blue.ThingType), elements)):
return View(elements, f'{self.__NAME}.{attr}', self.__PARENT)
elif all(map(lambda x: isinstance(x, (FunctionType, MethodType)), elements)):
return FunctionHandle(elements, f'{self.__NAME}.{attr}', self.__PARENT)
elif all(map(lambda x: x is None, elements)):
return [None] * len(elements) # []
else:
return elements
else:
try:
#elements = list((item for view in map(lambda x: x.__getattr__(attr), self) for item in view))
elements = list((item for view in map(lambda x: x.__getattribute__(attr), self) for item in view))
except AttributeError as error:
raise AttributeError(f'{self}.{attr} cannot be retrieved.') from None
return View(elements, f'{self.__NAME}.{attr}', self.__PARENT)
@blue.restrict
def __setattr__(self, attr: str, val: object) -> None:
"""
This method passes the setting of attributes to each individual Thing contained in View.
Depending on whether ``val`` is a list/View containing values for each Thing individually
or a singular value, ``val`` is either unrolled along the first axis or given fully for each
assignment.
>>> # keeping the view object from __getattr__
>>> view.bodies[1:].name = ['intermediate_index',
'intermediate_middle',
'intermediate_ring',
'intermediate_pinky']
>>> view.bodies.name
['intermediate_index',
'intermediate_middle',
'intermediate_ring',
'intermediate_pinky']
.. code-block:: mxml
:caption: XML structure
<body name="palm">
<body name="thumb">
<body name="distal_(0)" />
</body>
<body name="index">
<body name="intermediate_index">
<body name="distal_(1)" />
</body>
</body>
<body name="middle">
<body name="intermediate_middle">
<body name="distal_(2)" />
</body>
</body>
<body name="ring">
<body name="intermediate_ring">
<body name="distal_(3)" />
</body>
</body>
<body name="pinky">
<body name="intermediate_pinky">
<body name="distal_(4)" />
</body>
</body>
</body>
.. note::
If values should be unrolled over all Things, ``vals`` must have the same length as the View!
Parameters
----------
attr : str
The name of the attribute to be set.
val : object
Either a list View or other iterable that contains different values for all children or one value of any type that is set for all.
"""
if attr in self.__RESERVED:
return super().__setattr__(attr, val)
else:
view_attr = self.__getattr__(attr)
for element, elem_val in zip(self, self.__nesting_value(view_attr, val)):
element.__setattr__(attr, elem_val)
def __delattr__(self, attr):
"""
This method detaches the attributes and deletes them afterwards
"""
view_attr = self.__getattr__(attr)
if isinstance(view_attr, blue.ViewType):
view_attr.parent.detach.distribute(view_attr)
self.__PARENT._decouple_descendants()
def __delitem__(self, key):
"""
This method detaches the attributes and deletes them afterwards
"""
view_key = self[key]
if isinstance(view_key, blue.ViewType):
view_key.parent.detach.distribute(view_key)
view_key.parent._decouple_descendants()
self.__PARENT._decouple_descendants()
[docs]
def apply(self, function, *args, **kwargs) -> None|list:
"""
Applies a function to all Things contained in View. Arguments and keyword arguments
are passed to each individual call in an all to all relation.
>>> # keeping the view object from __setattr__
>>> renaming = lambda x: x.__setattr__('name', f'distal_{x.parent.parent.name}')
>>> view.bodies.bodies.apply(renaming)
>>> view.bodies.bodies.name
['distal_index', 'distal_middle', 'distal_ring', 'distal_pinky']
Parameters
----------
function : Callable
Functions methods and classes are valid arguments.
*args
Arguments passed to each individual call.
**kwargs
Kexword arguments passed to each individual call.
Returns
-------
None | list
If any individual call results in a value that is not None, a list of all returned values gets returned, None otherwise.
"""
results = [function(element) for element in self]
if any(x is not None for x in results):
return results
def __nesting_value(self, view_attr, val):
"""
Thie method is used to create an iterator for value assignment that either creates
a many to one or a one to one relation, depending on the new values shape and the
current attributes shape.
"""
view_structure = self.__nesting_structure(view_attr)
val_structure = self.__nesting_structure(val)
if isinstance(val, np.ndarray) and self.__compatible_numpy(view_structure, val):
return np.rollaxis(val, axis=0)
elif self.__compatible_nesting(view_structure, val_structure):
return val
elif isinstance(val, blue.ViewType):
if len(val) != 1:
raise ValueError(f'To set a View attribute to another View both sizes must be identical or the other Views size must be one, got sizes {len(view_structure)} and {len(val_structure)}.')
else:
return (val[0] for _ in range(len(self)))
else:
return (val for _ in range(len(view_structure)))
@classmethod
def __nesting_structure(cls, element):
"""
This method retrieves a representation of the elements nesting structure.
"""
if isinstance(element, np.ndarray):
return element.shape
elif isinstance(element, str):
return str
elif isinstance(element, View):
return cls.__nesting_structure(list(element))
elif hasattr(element, '__iter__'):
return element.__class__(map(cls.__nesting_structure, element))
else:
return element.__class__
@staticmethod
def __compatible_nesting(view_structure, val_structure):
"""
This method determines whether the two nesting structures are compatible for a
one to one assignment relation for lists and other non numpy iterables.
"""
val_class = val_structure if isinstance(val_structure, type) else val_structure.__class__
if not hasattr(val_structure, '__len__') or issubclass(val_class, str):
return False
elif len(val_structure) != len(view_structure):
return False
elif (not hasattr(val_structure, '__contains__') or view_structure not in val_structure) and val_structure != view_structure:
return False
else:
return True
@staticmethod
def __compatible_numpy(view_structure, val):
"""
This method determines whether the two nesting structures are compatible for a
one to one assignment relation for numpy arrays.
"""
if val.shape[0] == len(view_structure) and val.ndim > 1:
if all(isinstance(view, tuple) and view == val.shape[1:] for view in view_structure):
return True
elif all(isinstance(view, (list, tuple)) and len(view) == val.shape[1] for view in view_structure):
return True
return False
[docs]
class FunctionHandle(blue.FunctionHandleType):
"""
This class is used to handle access to a function or method called from a :class:`View`.
>>> palm.bodies
View[5|palm.bodies]
>>> palm.bodies.shift
ViewFunction[5|palm.bodies.shift]
Arguments given to the function or method call are passed to each individual Thing
in a many to one relation.
>>> palm.bodies.pos
[array([0., 0., 0.], dtype=float32),
array([0., 0., 0.], dtype=float32),
array([0., 0., 0.], dtype=float32),
array([0., 0., 0.], dtype=float32),
array([0., 0., 0.], dtype=float32)]
>>> many_to_one = palm.bodies.shift([2, 0, -1])
>>> many_to_one
View[5|palm.bodies.shift(...)]
>>> many_to_one.pos
[array([ 2., 0., -1.], dtype=float32),
array([ 2., 0., -1.], dtype=float32),
array([ 2., 0., -1.], dtype=float32),
array([ 2., 0., -1.], dtype=float32),
array([ 2., 0., -1.], dtype=float32)]
If the :meth:`distributed` over all Things contained in the parenting
View in a one to one relation by using the distribute submethod.
>>> one_to_one = palm.bodies.shift.distribute([[0, 0, 1],
[0, 1, 0],
[0, 1, 1],
[1, 0, 0],
[1, 0, 1]])
>>> one_to_one.pos
[array([0., 0., 1.], dtype=float32),
array([0., 1., 0.], dtype=float32),
array([0., 1., 1.], dtype=float32),
array([1., 0., 0.], dtype=float32),
array([1., 0., 1.], dtype=float32)]
If the result of all method or function calls are None, None is returned.
>>> palm.bodies.attach()
None
If all results are Things, a View of them is returned. This allows for
functions and methods that return Things to be chained.
>>> palm.bodies.copy()
View[5|palm.bodies.copy(...)]
>>> palm.bodies.shift([0, 0, 3]).rotate(alpha=PI).copy()
View[5|palm.bodies.shift(...).rotate(...).copy(...)]
Otherwise a list of all individually returned values is returned.
>>> palm.bodies._mujoco_specs()
[{'name': 'thumb'},
{'name': 'index'},
{'name': 'middle'},
{'name': 'ring'},
{'name': 'pinky'}]
"""
[docs]
def __init__(self, functions: list, name: str, parent: blue.ThingType) -> None:
"""
The FunctionHandle takes the functions or methods to be called, the name by which
it was called and the initial parent from which the first :class:`View` was called.
Parameters
----------
functions : list
All functions or methods from the Views Things.
name : str
The name of the called function or method.
parent : blue.ThingType
The parent from which the first View was called.
"""
self.__NAME = name
self.__PARENT = parent
self.__FUNCTIONS = functions
def __repr__(self) -> str:
"""
Represents the View as a string.
Returns
-------
str
The representing string contains a representation of the call that was used to get the View.
"""
return f'ViewFunction[{len(self)}|{self.__PARENT.name}.{self.__NAME}]'
def __iter__(self):
"""
Yields
------
Callable
The functions or methods Things that were requested from the View.
"""
for functions in self.__FUNCTIONS:
yield functions
def __len__(self) -> int:
"""
Returns
-------
int
The number of
"""
return len(self.__FUNCTIONS)
def __call__(self, *args, **kwargs) -> None|list:
"""
The call is redirected to each function or method in the FunctionHandle.
All arguments are passed on all calls in a many to one relation.
Parameters
----------
*args
The arguments to be passed to the call.
**kwargs
The keywordarguments to be passed to the call.
Returns
-------
None | list | View
The aggregated result of each call.
"""
result = [functions(*args, **kwargs) for functions in self]
return self._convert_result(result)
[docs]
def distribute(self, *args, **kwargs) -> None|list:
"""
The call is redirected to each function or method in the FunctionHandle.
The arguments are unrolled along the first axis and distributed over all calls
in a one to one relation.
Parameters
----------
*args
The arguments to be passed to the call.
**kwargs
The keywordarguments to be passed to the call.
Returns
-------
None | list | View
The aggregated result of each call.
"""
kw_list = [dict(zip(kwargs.keys(), vals)) for vals in zip(*kwargs.values())]
if args and kwargs:
assert all(len(self) == l for l in map(len, args))
assert len(self) == len(kw_list)
result = [function(*arg, **kwarg) for function, arg, kwarg in zip(self, zip(*args), kw_list)]
elif args:
assert all(len(self) == l for l in map(len, args))
result = [function(*arg) for function, arg in zip(self, zip(*args))]
elif kwargs:
assert len(self) == len(kw_list)
result = [function(**kwarg) for function, kwarg in zip(self, kw_list)]
else:
result = [function() for function in self]
return self._convert_result(result)
def _convert_result(self, result):
if any(x is not None for x in result):
if all(isinstance(x, blue.ThingType) for x in result):
return View(result, f'{self.__NAME}(...)', self.__PARENT)
else:
return result
else:
return None
[docs]
class AllView(blue.AllViewType, View):
"""
The AllView is analogous to the :class:`View` for all descendants instead of children.
Attribute getting and setting is handeled for all valid descendants. A ThingsType
attribute is then converted back to a normal view.
.. code-block:: mxml
:caption: XML Structure:
<body name="A">
<body name="B" />
<geom mass="42.0" type="sphere" name="C" />
</body>
.. code-block:: python
:caption: Children and Descendants View Comparison:
>>> world.bodies
View[1|anonymous_model.bodies]
>>> world.bodies.name
['A']
>>> world.all
View[3|anonymous_model.all]
>>> world.all.name
['A', 'B', 'C']
>>> world.all.bodies
View[2|anonymous_model.all.bodies]
>>> world.bodies.name
['A', 'B']
If only some descendants have a certain attribute, the view restricts to this valid subset.
For example, only :class:`Geoms <blueprints.geoms.BaseGeom>` have mass, so retrieving ``all.mass`` excludes all non-Geoms.
.. code-block:: python
:caption: Children and Descendants View Comparison:
>>> world.all.mass
[42.0]
"""
__RESERVED = {'__ELEMENTS',
'__NAME',
'__PARENT',
'__RESERVED',
'__DESCENDANTS',
'_View__ELEMENTS',
'_View__NAME',
'_View__PARENT',
'_View__RESERVED',
'_AllView__ELEMENTS',
'_AllView__NAME',
'_AllView__PARENT',
'_AllView__RESERVED',
'_AllView__DESCENDANTS'}
[docs]
@blue.restrict
def __init__(self, descendants: dict, parent: blue.ThingType) -> None:
"""
Parameters
----------
descendants : dict
The descendants property invoked on ``parent``
parent : blue.ThingType
The parent Thing from which ``.all`` was retrieved.
"""
elements = []
for desc_dict in descendants.values():
elements.extend(desc_dict['descendants'])
super().__init__(elements=elements, name='all', parent=parent)
self.__DESCENDANTS = descendants
def __getattr__(self, attr: str):
if attr in self.__RESERVED:
return super().__getattribute__(attr)
if attr in self.__DESCENDANTS:
return View(self.__DESCENDANTS[attr]['descendants'], f'{self._View__NAME}.{attr}', self._View__PARENT)
else:
if not all(hasattr(thing, attr) for thing in self):
elements = list(filter(lambda x: hasattr(x, attr), self))
view = View(elements, name=self._View__NAME, parent=self._View__PARENT)
return view.__getattr__(attr)
else:
return super().__getattr__(attr)
@blue.restrict
def __setattr__(self, attr: str, val: object) -> None:
super().__setattr__(attr, val)
if attr in self.__RESERVED or all(hasattr(thing, attr) for thing in self):
super().__setattr__(attr, val)
else:
elements = list(filter(lambda x: hasattr(x, attr), self))
view = View(elements, name=self._View__NAME, parent=self._View__PARENT)
view.__setattr__(attr, val)
[docs]
class LatticeView(blue.LatticeViewType):
"""
A LatticeView is created whenever a :class:`Lattice <blueprints.utils.lattice.Lattice>` is indexed.
Attribute getter and setter are handled through this View similar to :class:`View`. For details have
a look at :class:`Lattice <blueprints.utils.lattice.Lattice>`.
"""
__RESERVED = {'_LatticeView__THINGS',
'_LatticeView__KEYS',
'_LatticeView__KEY_STR',
'_LatticeView__PARENT'}
[docs]
def __init__(self,
things: list,
parent: blue.LatticeType,
keys: tuple[int|slice] = ()):
"""
Parameters
----------
things : list
The indexed Things of the parent Lattice
parent : blue.LatticeType
The Lattice which was indexed to create the LatticeView
keys : tuple[int | slice]
The keys used to index the Lattice
"""
def parse(key):
if isinstance(key, int):
return str(key)
elif isinstance(key, slice):
indecies = []
if key.step is not None:
indecies.insert(0, str(key.step))
if key.stop is not None:
indecies.insert(0, str(key.stop))
else:
indecies.insert(0, '')
if key.start is not None:
indecies.insert(0, str(key.start))
else:
indecies.insert(0, '')
return ':'.join(indecies)
else:
raise NotImplemented
self.__KEY_STR = ', '.join(map(parse, keys))
self.__KEYS = keys
self.__THINGS = things if isinstance(things, list) else [things]
self.__PARENT = parent
@property
def n_dim(self) -> int:
"""
The number of dimensions in the Lattice
Returns
-------
int
"""
things = self.__THINGS
n = 0
while isinstance(things, list):
n += 1
things = things[0]
return n
@blue.restrict
def __getitem__(self, keys: int|slice|tuple[int|slice]) -> blue.LatticeViewType|blue.MoveableThingType:
if isinstance(keys, tuple) and len(keys) > self.n_dim:
raise IndexError(f'Lattice {self} has {self.n_dim} axes but {len(keys)} were indexed.')
if isinstance(keys, (int, slice)):
keys = (keys,)
def rec(things, keys):
if len(keys) == 0:
return things
else:
key, keys = keys[0], keys[1:]
if isinstance(key, int):
return rec(things[key], keys)
else:
return [rec(thing, keys) for thing in things[key]]
if all(isinstance(key, int) for key in keys) and len(keys) == self.n_dim:
return rec(self.__THINGS, keys)
return blue.LatticeView(rec(self.__THINGS, keys), self.__PARENT, keys)
def __iter__(self) -> blue.ThingType:
queue = deque(self.__THINGS)
while queue:
item = queue.popleft()
if isinstance(item, list):
queue.extendleft(reversed(item))
else:
yield item
def __len__(self) -> int:
"""
Returns
-------
int
The number of Things contained in the LatticeView.
"""
things = self.__THINGS
l = 1 if things else 0
while isinstance(things, list):
l *= len(things)
things = things[0]
return l
def __repr__(self) -> str:
"""
Represents the View as a string.
Returns
-------
str
The representing string contains a representation of the call that was used to get the View.
"""
if self.__KEY_STR:
return f'LatticeView[{self.__KEY_STR}]'
else:
return 'LatticeView'
def __getattr__(self, attr):
if len(self) == 0:
raise AttributeError(f'{self} is empty, attribute "{attr}" cannot be retrieved.')
if all(hasattr(x, attr) for x in self):
things = self._rec(lambda x: getattr(x, attr), list, self.__THINGS)
if self._rec(lambda x: isinstance(x, blue.ViewType), all, things):
things = list(self._rec(list, lambda x: list(chain(*x)), things))
return View(things, attr, self.__PARENT)
elif self._rec(lambda x: isinstance(x, blue.ThingType), all, things):
things = list(self._rec(lambda x: [x], lambda x: list(chain(*x)), things))
return View(things, attr, self.__PARENT)
elif self._rec(lambda x: isinstance(x, (FunctionType, MethodType)), all, things):
things = list(self._rec(lambda x: [x], lambda x: list(chain(*x)), things))
return FunctionHandle(things, attr, self.__PARENT)
elif self._rec(lambda x: x is None, all, things):
return [None] * len(self) # []
else:
return things
else:
try:
things = self._rec(lambda x: x.__getattribute__(attr), list, self.__THINGS)
except AttributeError as error:
raise AttributeError(f'{self}.{attr} cannot be retrieved.') from None
return View(things, attr, self.__PARENT)
def __setattr__(self, attr, value):
if attr in self.__RESERVED:
super().__setattr__(attr, value)
else:
view_attr = self.__getattr__(attr)
view_structure = self.__nesting_structure(view_attr)
val_structure = self.__nesting_structure(value)
if view_structure == val_structure:
def rec(func, things, values):
if isinstance(things, list):
for thing, value in zip(things, values):
rec(func, thing, value)
else:
func(things, values)
rec(lambda thing, value: thing.__setattr__(attr, value), self.__THINGS, value)
else:
def rec(func, things):
if isinstance(things, list):
for thing in things:
rec(func, thing)
else:
func(things)
rec(lambda thing: thing.__setattr__(attr, value), self.__THINGS)
@classmethod
def _rec(cls, func, aggregate, things):
"""
This classmethod is a helper to apply a function on individual Things and aggregate the results.
"""
if isinstance(things, list):
return aggregate(cls._rec(func, aggregate, thing) for thing in things)
else:
return func(things)
@classmethod
def __nesting_structure(cls, element):
"""
This method retrieves a representation of the elements nesting structure.
"""
if isinstance(element, np.ndarray):
return element.shape
elif isinstance(element, str):
return str
elif isinstance(element, View):
return cls.__nesting_structure(list(element))
elif hasattr(element, '__iter__'):
return element.__class__(map(cls.__nesting_structure, element))
else:
return element.__class__