baldertest 0.1.0b10__py3-none-any.whl → 0.1.0b12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. _balder/_version.py +1 -1
  2. _balder/cnnrelations/__init__.py +7 -0
  3. _balder/cnnrelations/and_connection_relation.py +149 -0
  4. _balder/cnnrelations/base_connection_relation.py +270 -0
  5. _balder/cnnrelations/or_connection_relation.py +65 -0
  6. _balder/collector.py +10 -16
  7. _balder/connection.py +400 -881
  8. _balder/connection_metadata.py +255 -0
  9. _balder/controllers/device_controller.py +37 -16
  10. _balder/controllers/feature_controller.py +63 -99
  11. _balder/controllers/normal_scenario_setup_controller.py +5 -5
  12. _balder/controllers/scenario_controller.py +6 -6
  13. _balder/controllers/setup_controller.py +2 -3
  14. _balder/decorator_connect.py +12 -10
  15. _balder/decorator_for_vdevice.py +17 -25
  16. _balder/decorator_gateway.py +3 -3
  17. _balder/executor/testcase_executor.py +0 -1
  18. _balder/executor/variation_executor.py +212 -199
  19. _balder/feature.py +1 -1
  20. _balder/feature_replacement_mapping.py +69 -0
  21. _balder/feature_vdevice_mapping.py +88 -0
  22. _balder/fixture_manager.py +10 -9
  23. _balder/objects/connections/osi_3_network.py +2 -2
  24. _balder/objects/connections/osi_4_transport.py +2 -2
  25. _balder/routing_path.py +27 -31
  26. _balder/solver.py +1 -1
  27. _balder/testresult.py +1 -1
  28. _balder/utils.py +27 -1
  29. {baldertest-0.1.0b10.dist-info → baldertest-0.1.0b12.dist-info}/METADATA +2 -2
  30. {baldertest-0.1.0b10.dist-info → baldertest-0.1.0b12.dist-info}/RECORD +34 -27
  31. {baldertest-0.1.0b10.dist-info → baldertest-0.1.0b12.dist-info}/WHEEL +1 -1
  32. {baldertest-0.1.0b10.dist-info → baldertest-0.1.0b12.dist-info}/LICENSE +0 -0
  33. {baldertest-0.1.0b10.dist-info → baldertest-0.1.0b12.dist-info}/entry_points.txt +0 -0
  34. {baldertest-0.1.0b10.dist-info → baldertest-0.1.0b12.dist-info}/top_level.txt +0 -0
_balder/connection.py CHANGED
@@ -3,11 +3,27 @@ from typing import List, Tuple, Union, Type, Dict
3
3
 
4
4
  import copy
5
5
  import itertools
6
+
7
+ from _balder.connection_metadata import ConnectionMetadata
6
8
  from _balder.device import Device
7
9
  from _balder.exceptions import IllegalConnectionTypeError
10
+ from _balder.cnnrelations import AndConnectionRelation, OrConnectionRelation
11
+ from _balder.utils import cnn_type_check_and_convert
12
+
13
+
14
+ class ConnectionType(type):
15
+ """
16
+ Metaclass for :class:`Connection` objects
17
+ """
18
+
19
+ def __and__(cls, other) -> AndConnectionRelation:
20
+ return cls().__and__(other)
21
+
22
+ def __or__(cls, other) -> OrConnectionRelation:
23
+ return cls().__or__(other)
8
24
 
9
25
 
10
- class Connection:
26
+ class Connection(metaclass=ConnectionType):
11
27
  """
12
28
  This is the basic connection class. On one side it is the common base class for all connection objects. On the other
13
29
  side it can also be used to describe a container connection, that contains a list of different connection items.
@@ -23,7 +39,7 @@ class Connection:
23
39
  .. note::
24
40
  With a direct instance of a :class:`Connection` object you can create an own Connection-Tree. You can use
25
41
  this container object, if you need a container for a list of :class:`Connection`-Trees that are combined
26
- with an AND (tuple) or/and an OR (list).
42
+ with an AND (``&``) or/and an OR (``|``).
27
43
 
28
44
  :param from_device: the device this connection starts from (default: None)
29
45
 
@@ -33,47 +49,35 @@ class Connection:
33
49
 
34
50
  :param to_device_node_name: the node name of the device the connection ends
35
51
  """
52
+ # note: currently every connection is bidirectional (we want to add support for this later)
36
53
 
37
54
  # contains all metadata of this connection object
38
- self._metadata = {
39
- "from_device": None, "from_device_node_name": None, "to_device": None, "to_device_node_name": None
40
- }
55
+ self._metadata = ConnectionMetadata(
56
+ from_device=from_device,
57
+ to_device=to_device,
58
+ from_device_node_name=from_device_node_name,
59
+ to_device_node_name=to_device_node_name,
60
+ bidirectional=True
61
+ )
41
62
 
42
- self._bidirectional = None
43
63
  # contains all sub connection objects, this connection tree is based on
44
- self._based_on_connections = []
64
+ self._based_on_connections = OrConnectionRelation()
45
65
 
46
- if from_device is not None and not issubclass(from_device, Device):
47
- raise TypeError(f"detect illegal argument element {str(from_device)} for given attribute "
48
- f"`from_device` - should be a subclasses of `balder.Device`")
49
- self._metadata["from_device"] = from_device
66
+ def __and__(self, other: Union[Connection, AndConnectionRelation, OrConnectionRelation]) -> AndConnectionRelation:
67
+ new_list = AndConnectionRelation(self)
50
68
 
51
- if from_device_node_name is not None and not isinstance(from_device_node_name, str):
52
- raise TypeError(f"detect illegal argument type {type(from_device_node_name)} for given attribute "
53
- f"`from_device_node_name` - should be a string value")
54
- self._metadata["from_device_node_name"] = from_device_node_name
69
+ other = cnn_type_check_and_convert(other)
70
+ new_list.append(other)
55
71
 
56
- if to_device is not None and not issubclass(to_device, Device):
57
- raise TypeError(f"detect illegal argument element {str(to_device)} for given attribute "
58
- f"`to_device` - should be a subclasses of `balder.Device`")
59
- self._metadata["to_device"] = to_device
72
+ return new_list
60
73
 
61
- if to_device_node_name is not None and not isinstance(to_device_node_name, str):
62
- raise TypeError(f"detect illegal argument type {type(to_device_node_name)} for given attribute "
63
- f"`to_device_node_name` - should be a string value")
64
- self._metadata["to_device_node_name"] = to_device_node_name
74
+ def __or__(self, other: Union[Connection, AndConnectionRelation, OrConnectionRelation]) -> OrConnectionRelation:
75
+ new_list = OrConnectionRelation(self)
65
76
 
66
- # describes if the connection is uni or bidirectional
67
- # note: currently every connection is bidirectional (we want to add support for this later)
68
- self._bidirectional = True
77
+ other = cnn_type_check_and_convert(other)
78
+ new_list.append(other)
69
79
 
70
- if not ((from_device is None and to_device is None and from_device_node_name is None
71
- and to_device_node_name is None) or (
72
- from_device is not None and to_device is not None and from_device_node_name is not None and
73
- to_device_node_name is not None)):
74
- raise ValueError(
75
- "you have to provide all or none of the following items: `from_device`, `from_device_node_name`, "
76
- "`to_device` or `to_device_node_name`")
80
+ return new_list
77
81
 
78
82
  def __eq__(self, other):
79
83
  if isinstance(other, Connection):
@@ -82,202 +86,12 @@ class Connection:
82
86
  return False
83
87
 
84
88
  def __hash__(self):
85
- all_hashes = hash(self.from_device) + hash(self.to_device) + hash(self.from_node_name) + \
86
- hash(self.to_node_name) + hash(str(self))
87
- for cur_child in self.based_on_elements:
88
- all_hashes += hash(cur_child)
89
+ all_hashes = hash(self.metadata) + hash(self.based_on_elements)
89
90
  return hash(all_hashes)
90
91
 
91
- def clone_without_based_on_elements(self) -> Connection:
92
- """
93
- This method returns a copied version of this element, while all `_based_on_connections` are removed (the copied
94
- element has an empty list here).
95
-
96
- :return: a python copied object of this item
97
- """
98
- self_copy = copy.copy(self)
99
- self_copy._based_on_connections = [] # pylint: disable=protected-access
100
- return self_copy
101
-
102
- def clone(self) -> Connection:
103
- """
104
- This method returns an exact clone of this connection. For this clone every inner connection object will be
105
- newly instantiated, but all internal references (like the devices and so on) will not be copied (objects are the
106
- same for this object and the clone). The method will make a normal copy for every connection object in the
107
- `_based_on_elements` list.
108
- """
109
- self_copy = self.clone_without_based_on_elements()
110
-
111
- for cur_based_on in self._based_on_connections:
112
- if isinstance(cur_based_on, tuple):
113
- cloned_tuple = tuple([cur_tuple_element.clone() for cur_tuple_element in cur_based_on])
114
- self_copy.append_to_based_on(cloned_tuple)
115
- elif isinstance(cur_based_on, Connection):
116
- cloned_cur_based_on = cur_based_on.clone()
117
- self_copy.append_to_based_on(cloned_cur_based_on)
118
- else:
119
- raise TypeError('based on element is not from valid type')
120
- return self_copy
121
-
122
- @staticmethod
123
- def __cut_conn_from_only_parent_to_child(elem: Connection) -> List[Connection]:
124
- """
125
- This helper method returns all possible pieces while the base element will remain intact - so every returned
126
- element is from the same type as `elem` (only the related `based_on_elements` are changed).
127
-
128
- .. note::
129
- The given element itself will also be added here!
130
-
131
- .. note::
132
- The given element has to be single!
133
- """
134
- all_pieces = [elem.clone()]
135
- if len(elem.based_on_elements) == 0:
136
- return all_pieces
137
- # if the next element is a tuple -> call procedure for tuples and add a copy of this object as child
138
- if isinstance(elem.based_on_elements[0], tuple):
139
- # return all possibilities of the tuple while adding the current object as child of the tuple
140
- for cur_tuple in Connection.__cut_tuple_from_only_parent_to_child(elem.based_on_elements[0]):
141
- # for this we do not have to use `clone`, because we copy the base object with `copy.copy` and
142
- # completely replace the `_based_on_connections` by our own
143
- copied_conn = elem.clone_without_based_on_elements()
144
- copied_conn.append_to_based_on(cur_tuple)
145
- all_pieces.append(copied_conn)
146
- return all_pieces
147
- # if this element is the last element with a parent -> copy it and remove the parent, return it
148
- if len(elem.based_on_elements[0].based_on_elements) == 0:
149
- new_elem = elem.clone_without_based_on_elements()
150
- all_pieces.append(new_elem)
151
- return all_pieces
152
- # otherwise, the current item has grandparents, so call the method recursively for parents and add a copy of
153
- # this object as child
154
- all_possible_parents = Connection.__cut_conn_from_only_parent_to_child(elem.based_on_elements[0])
155
- for cur_parent in all_possible_parents:
156
- copied_conn = elem.clone_without_based_on_elements()
157
- copied_conn.append_to_based_on(cur_parent)
158
- all_pieces.append(copied_conn)
159
- return all_pieces
160
-
161
- @staticmethod
162
- def __cut_tuple_from_only_parent_to_child(elem: Tuple[Connection]) -> List[Tuple[Connection]]:
163
- """
164
- This helper method returns all possible pieces while the base elements of the tuple will remain intact - so
165
- every returned tuple element is from same type like the related elements in the tuple `elem` (only the related
166
- `based_on_elements` are changed).
167
-
168
- .. note::
169
- The given element has to be single!
170
-
171
- .. note::
172
- Note that this method also returns every possible ordering
173
- """
174
-
175
- tuple_with_all_possibilities = (
176
- tuple([Connection.__cut_conn_from_only_parent_to_child(cur_tuple_item) for cur_tuple_item in elem]))
177
-
178
- cloned_tuple_list = []
179
- for cur_tuple in list(itertools.product(*tuple_with_all_possibilities)):
180
- cloned_tuple = tuple([cur_tuple_item.clone() for cur_tuple_item in cur_tuple])
181
- cloned_tuple_list.append(cloned_tuple)
182
- return cloned_tuple_list
183
-
184
- @staticmethod
185
- def check_if_tuple_contained_in_connection(tuple_elem: Tuple[Connection], other_elem: Connection) -> bool:
186
- """
187
- This method checks if the tuple given by `tuple_elem` is contained in the `other_elem`. To ensure that a tuple
188
- element is contained in a connection tree, there has to be another tuple into the `other_elem`, that has the
189
- same length or is bigger. In addition, there has to exist an order combination where every element of the
190
- `tuple_elem` is contained in the found tuple in `other_elem`. In this case it doesn't matter where the tuple is
191
- in `other_elem` (will be converted to single, and tuple will be searched in all BASED_ON elements). If the tuple
192
- element of `other_elem` has fewer items than our `tuple_elem`, it will be ignored. The method only search for
193
- a valid existing item in the `other_elem` tuple for every item of the `tuple_elem`.
194
-
195
- :param tuple_elem: the tuple element that should be contained in the `other_elem`
196
-
197
- :param other_elem: the connection object, the given tuple should be contained in
198
- """
199
- def tuple_is_contained_in_other(inner_tuple, contained_in_tuple):
200
- # check if every tuple elem fits in one of `contained_in_tuple` (allow to use a position in
201
- # `contained_in_tuple` multiple times)
202
- for cur_tuple_element in inner_tuple:
203
- found_match_for_this_elem = False
204
- for cur_contained_in_elem in contained_in_tuple:
205
- if cur_tuple_element.contained_in(cur_contained_in_elem, ignore_metadata=True):
206
- found_match_for_this_elem = True
207
- break
208
- if not found_match_for_this_elem:
209
- return False
210
- return True
211
-
212
- other_singles = other_elem.get_singles()
213
- tuple_singles = Connection.convert_tuple_to_singles(tuple_elem)
214
- for cur_tuple_single in tuple_singles:
215
- for cur_other_single in other_singles:
216
- # check if we found a tuple element in the other object -> go the single connection upwards and search
217
- # for a tuple
218
- cur_sub_other_single: List[Union[Connection, Tuple[Connection]]] = [cur_other_single]
219
- while len(cur_sub_other_single) > 0:
220
- if isinstance(cur_sub_other_single[0], tuple):
221
- # found a tuple -> check if length does match
222
- if len(cur_sub_other_single[0]) >= len(cur_tuple_single) and tuple_is_contained_in_other(
223
- cur_tuple_single, contained_in_tuple=cur_sub_other_single[0]):
224
- return True
225
- # otherwise, this complete element is not possible - skip this single!
226
- break
227
- cur_sub_other_single = cur_sub_other_single[0].based_on_elements
228
- return False
229
-
230
- @staticmethod
231
- def convert_tuple_to_singles(tuple_elem: Tuple[Connection]) -> List[Union[Connection, Tuple[Connection]]]:
232
- """
233
- This method converts the given `tuple_elem` to single items and return these.
234
-
235
- :param tuple_elem: the tuple element out of which the single items are being created
236
-
237
- :returns: a list of new connection objects that are single items
238
- """
239
- singles_tuple = ()
240
- tuple_idx = 0
241
- for cur_tuple_elem in tuple_elem:
242
- if not isinstance(cur_tuple_elem, Connection):
243
- raise TypeError(f"the tuple element at index {tuple_idx} of element from `other_conn` is not "
244
- "from type `Connection`")
245
- # get all singles of this tuple element
246
- singles_tuple += (cur_tuple_elem.get_singles(),)
247
- tuple_idx += 1
248
- # now get the variations and add them to our results
249
- return list(itertools.product(*singles_tuple))
250
-
251
- @staticmethod
252
- def cleanup_connection_list(full_list: List[Union[Connection, Tuple[Connection]]]) \
253
- -> List[Union[Connection, Tuple[Connection]]]:
254
- """
255
- This method cleanup a connection list while removing items that are direct duplicates and by removing duplicates
256
- that are fully contained_in other items.
257
-
258
- :param full_list: the full list of connections and tuples of connections that should be cleaned-up
259
-
260
- :returns: returns the cleaned up list
261
- """
262
- result = full_list.copy()
263
- next_loop = True
264
- while not next_loop:
265
- next_loop = False
266
- for cur_elem in result:
267
- all_other_elems = [cur_item for cur_item in result if cur_item != cur_elem]
268
- if isinstance(cur_elem, Connection):
269
- for cur_other_elem in all_other_elems:
270
- # check if it contained in or the same
271
- if cur_elem.contained_in(cur_other_elem, ignore_metadata=True):
272
- # we can remove it from result list
273
- result.remove(cur_elem)
274
- next_loop = True
275
- break
276
- return result
277
-
278
92
  # ---------------------------------- STATIC METHODS ----------------------------------------------------------------
279
93
 
280
- # ---------------------------------- CLASS METHODS ----------------------------------------------------------------
94
+ # ---------------------------------- CLASS METHODS -----------------------------------------------------------------
281
95
 
282
96
  @classmethod
283
97
  def get_parents(cls, tree_name: Union[str, None] = None) -> List[Type[Connection]]:
@@ -337,342 +151,206 @@ class Connection:
337
151
  return False
338
152
 
339
153
  @classmethod
340
- def based_on(cls, *args: Union[Tuple[Union[Type[Connection], Connection], ...], Type[Connection], Connection]) \
341
- -> Connection:
154
+ def based_on(
155
+ cls,
156
+ connection: Union[Connection, Type[Connection], AndConnectionRelation, OrConnectionRelation]
157
+ ) -> Connection:
342
158
  """
343
159
  With this method it is possible to define several sublayers of the connection. You can pass various other
344
160
  connections in this method as arguments.
345
161
 
346
- Note that multiple parameters of this method (but also of all other methods that work with Connections) mean
347
- an OR operation. So if you define a `BaseConnType.based_on(ConnType1, ConnType2)` it means that your connection
348
- `BaseConnType` is based on a `ConnType1` or a `ConnType2`.
162
+ Note that you can define logical statements with the `|` and the `&` operator.
163
+ For example: ``Connection1() or Connection2() and Connection3()``.
349
164
 
350
- In order to describe an AND operation, i.e. that the `BaseConnType` is based on a` ConnType1` and a `ConnType2`,
351
- you have to pass them as a tuple. This would look like this: `BaseConnType.based_on((ConnType1, ConnType2))`
352
-
353
- :param args: all connection items for which this connection is based on
165
+ :param connection: all connection items for which this connection is based on
354
166
 
355
167
  """
356
168
  this_instance = cls()
357
169
 
170
+ if isinstance(connection, type) and issubclass(connection, Connection):
171
+ connection = connection()
358
172
  new_items = []
359
- for cur_item in args:
360
- if isinstance(cur_item, Connection):
361
- if cur_item.__class__ == Connection:
362
- # it is a container -> add elements
363
- new_items += cur_item.based_on_elements
364
- else:
365
- new_items.append(cur_item)
366
- elif isinstance(cur_item, type) and issubclass(cur_item, Connection):
367
- new_items.append(cur_item())
368
- else:
369
- new_items.append(cur_item)
173
+ if isinstance(connection, Connection):
174
+ new_items.append(connection)
175
+ elif isinstance(connection, AndConnectionRelation):
176
+ new_items.append(connection)
177
+ elif isinstance(connection, OrConnectionRelation):
178
+ for cur_inner_elem in connection.connections:
179
+ new_items.append(cur_inner_elem)
180
+ else:
181
+ raise TypeError(f'can not use object from type {connection}')
182
+
370
183
  # do not create a container connection if no container is required here
371
- if cls == Connection and len(new_items) == 1 and not isinstance(new_items[0], tuple):
184
+ if cls == Connection and len(new_items) == 1 and isinstance(new_items[0], Connection):
372
185
  return new_items[0]
373
186
  this_instance.append_to_based_on(*new_items)
374
187
  return this_instance
375
188
 
376
189
  @classmethod
377
- def check_equal_connections_are_in(
378
- cls, cnns_from: List[Connection], are_in_cnns_from: List[Connection], ignore_metadata: bool=False) -> bool:
379
- """
380
- This method validates that the elements from the first list are contained (are equal) with one of the elements
381
- in the second list.
382
-
383
- .. note::
384
- This method only checks that every single connections from the first element is contained in the second too.
385
- It does not check the other direction. If you want to validate this, you need to call this method with both
386
- possibilities.
387
-
388
- :param cnns_from: the first list of connections
389
- :param are_in_cnns_from: the second list of connection
390
- :param ignore_metadata: True, if the metadata of the single connections should be ignored
391
- :return: True in case that every connection of the first list is equal with one in the second list, otherwise
392
- False
393
- """
394
- for cur_cnn in cnns_from:
395
- found_equal = False
396
- for cur_other_cnn in are_in_cnns_from:
397
- if cur_cnn.equal_with(cur_other_cnn, ignore_metadata=ignore_metadata):
398
- found_equal = True
399
- break
400
- if not found_equal:
401
- return False
402
- return True
403
-
404
- @classmethod
405
- def check_equal_connection_tuples_are_in(
406
- cls, tuples_from: List[Tuple[Connection]], are_in_tuples_from: List[Tuple[Connection]],
407
- ignore_metadata: bool=False) -> bool:
408
- """
409
- This method validates that the connection tuples from the first list are contained (are equal) within the tuple
410
- elements of the second list.
411
-
412
- .. note::
413
- This method only checks that every single tuple from the first element is contained in the second list.
414
- It does not check the other direction. If you want to validate this, you need to call this method with both
415
- possibilities.
416
-
417
- :param tuples_from: the first list of connection-tuples
418
- :param are_in_tuples_from: the second list of connection-tuples
419
- :param ignore_metadata: True, if the metadata of the single connections should be ignored
420
- :return: True in case that every tuple connections of the first list is equal with one tuple in the second,
421
- otherwise False
422
- """
423
- for cur_search_tuple in tuples_from:
424
- found_match_for_cur_search_tuple = False
425
- # go through each unmatched other tuple
426
- for cur_other_tuple in are_in_tuples_from:
427
- cur_search_tuple_is_completely_in_other_tuple = True
428
- for cur_search_tuple_elem in cur_search_tuple:
429
- fount_it = False
430
- for cur_other_tuple_elem in cur_other_tuple:
431
- if cur_search_tuple_elem.equal_with(cur_other_tuple_elem, ignore_metadata=ignore_metadata):
432
- fount_it = True
433
- break
434
- if not fount_it:
435
- cur_search_tuple_is_completely_in_other_tuple = False
436
- break
437
- if cur_search_tuple_is_completely_in_other_tuple:
438
- # here we have no match
439
- found_match_for_cur_search_tuple = True
440
- break
441
- if not found_match_for_cur_search_tuple:
442
- return False
443
- return True
190
+ def filter_connections_that_are_contained_in(
191
+ cls,
192
+ cnns_from: List[Union[Connection, AndConnectionRelation, OrConnectionRelation]],
193
+ are_contained_in: Connection,
194
+ ignore_metadata: bool = False
195
+ ) -> List[Connection]:
196
+ """
197
+ This method filters the connection elements from the first list to include only those connections that are
198
+ contained within the provided connection ``are_contained_in``.
199
+
200
+ :param cnns_from: a list of connections
201
+ :param are_contained_in: the connection, the connection elements should be contained in
202
+ :param ignore_metadata: True, if the metadata should be ignored
203
+ :return: a list with the filtered connections
204
+ """
205
+ return [
206
+ cnn for cnn in cnns_from
207
+ if cnn.contained_in(other_conn=are_contained_in, ignore_metadata=ignore_metadata)
208
+ ]
444
209
 
445
210
  # ---------------------------------- PROPERTIES --------------------------------------------------------------------
446
211
 
447
212
  @property
448
- def metadata(self) -> dict:
213
+ def metadata(self) -> ConnectionMetadata:
449
214
  """returns the connection metadata dictionary"""
450
215
  return self._metadata
451
216
 
452
- @metadata.setter
453
- def metadata(self, data):
454
- empty_metadata = {
455
- "from_device": None, "to_device": None, "from_device_node_name": None, "to_device_node_name": None}
456
-
457
- if not isinstance(data, dict):
458
- raise ValueError("the given metadata value has to be a dictionary")
459
- if data != {}:
460
- if sorted(["from_device", "to_device", "from_device_node_name", "to_device_node_name"]) != \
461
- sorted(list(data.keys())):
462
- raise ValueError("if you provide a metadata dictionary you have to provide all elements of it")
463
- else:
464
- data = empty_metadata.copy()
465
-
466
- # only allow to set the metadata dictionary if the old one has the same values or was empty before (no values)
467
- if data != empty_metadata:
468
- if self._metadata == empty_metadata:
469
- # it is ok, because the dictionary was empty before
470
- pass
471
- elif self._metadata == data:
472
- # it is ok too, because the new set data is the same as the data was before
473
- pass
474
- else:
475
- raise ValueError("you can not set another metadata than the data set before - please reset it first")
476
-
477
- self._metadata = data
478
-
479
217
  @property
480
218
  def from_device(self):
481
219
  """device from which the connection starts"""
482
- return self._metadata["from_device"]
220
+ return self._metadata.from_device if self._metadata else None
483
221
 
484
222
  @property
485
223
  def to_device(self):
486
224
  """device at which the connection ends"""
487
- return self._metadata["to_device"]
225
+ return self._metadata.to_device if self._metadata else None
488
226
 
489
227
  @property
490
228
  def from_node_name(self):
491
229
  """the name of the node in the `Device` from which the connection starts"""
492
- return self._metadata["from_device_node_name"]
230
+ return self._metadata.from_node_name if self._metadata else None
493
231
 
494
232
  @property
495
233
  def to_node_name(self):
496
234
  """the name of the node in the `Device` at which the connection ends"""
497
- return self._metadata["to_device_node_name"]
235
+ return self._metadata.to_node_name if self._metadata else None
498
236
 
499
237
  @property
500
- def based_on_elements(self) -> List[Union[Connection, Tuple[Connection]]]:
238
+ def based_on_elements(self) -> OrConnectionRelation:
501
239
  """returns a copy of the internal based_on_connection"""
502
- return self._based_on_connections.copy()
240
+ return self._based_on_connections.clone()
503
241
 
504
242
  # ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
505
243
 
506
- def _metadata_contained_in(self, of_connection: Connection) -> bool:
507
- """
508
- This method returns true if the metadata of the current connection is contained in the given one.
509
-
510
- The method returns true in the following situations:
511
- * both connections are bidirectional and the from and to elements (device and node name) are the same
512
- * both connections are unidirectional and have the same from and to elements
513
- * both connections are bidirectional and the from is the to and the to is the from
514
- * one connection is unidirectional and the other is bidirectional and the from and to elements are the same
515
- * one connection is unidirectional and the other is bidirectional and the from is the to and the to is the from
516
-
517
- :return: true if the metadata of the current connection is contained in the metadata of the given one
518
- """
519
- check_same = \
520
- self.from_device == of_connection.from_device and self.from_node_name == of_connection.from_node_name and \
521
- self.to_device == of_connection.to_device and self.to_node_name == of_connection.to_node_name
522
- check_opposite = \
523
- self.from_device == of_connection.to_device and self.from_node_name == of_connection.to_node_name and \
524
- self.to_device == of_connection.from_device and self.to_node_name == of_connection.from_node_name
525
- if self.is_bidirectional() and of_connection.is_bidirectional():
526
- return check_same or check_opposite
527
-
528
- if self.is_bidirectional() and not of_connection.is_bidirectional() or \
529
- not self.is_bidirectional() and of_connection.is_bidirectional():
530
- return check_same or check_opposite
531
-
532
- return check_same
533
-
534
- def _metadata_equal_with(self, of_connection: Connection) -> bool:
535
- """
536
- This method returns true if the metadata of the current connection is equal with the metadata of the given
537
- connection.
538
-
539
- The method returns true in the following situations:
540
- * both connections are bidirectional and the from and to elements (device and node name) are the same
541
- * both connections are unidirectional and have the same from and to elements
542
- * both connections are bidirectional and the from is the to and the to is the from
543
-
544
- :return: true if the metadata of the current connection is contained in the metadata of the given one
545
- """
546
- check_same = \
547
- self.from_device == of_connection.from_device and self.from_node_name == of_connection.from_node_name and \
548
- self.to_device == of_connection.to_device and self.to_node_name == of_connection.to_node_name
549
- check_opposite = \
550
- self.from_device == of_connection.to_device and self.from_node_name == of_connection.to_node_name and \
551
- self.to_device == of_connection.from_device and self.to_node_name == of_connection.from_node_name
552
- if self.is_bidirectional() and of_connection.is_bidirectional() or \
553
- not self.is_bidirectional() and not of_connection.is_bidirectional():
554
- return check_same or check_opposite
555
-
556
- return False
244
+ # ---------------------------------- METHODS -----------------------------------------------------------------------
557
245
 
558
- def get_intersection_with_other_single(self, other_conn: Union[Connection, Tuple[Connection]]) \
559
- -> List[Connection, Tuple[Connection]]:
246
+ def get_intersection_with_other_single(
247
+ self,
248
+ other_conn: Union[Connection, AndConnectionRelation, OrConnectionRelation]
249
+ ) -> List[Connection]:
560
250
  """
561
251
  A helper method that returns an intersection between the two connections (self and the given one).
562
252
 
563
- :param other_conn: the other **single** connection object (could be a tuple too, but note that this has to have
564
- only **single** elements!)
253
+ :param other_conn: the other **single** connection object (could be a relation too, but note that them needs to
254
+ be **single** elements!)
565
255
  """
566
256
  if not self.is_single():
567
257
  raise ValueError(
568
258
  "the current connection object is not single -> method only possible for single connections")
569
- if isinstance(other_conn, Connection):
570
- if other_conn.__class__ == Connection:
571
- raise ValueError("a container object from direct class `Connection` is not allowed here - please "
572
- "provide a single connection or a single tuple")
573
- if not other_conn.is_single():
574
- raise ValueError(
575
- "the connection object given by `other_conn` is not single -> method only possible for single "
576
- "connections")
577
- elif isinstance(other_conn, tuple):
578
- # has to be a tuple
579
- cur_idx = 0
580
- for cur_tuple_element in other_conn:
581
- if not cur_tuple_element.is_single():
582
- raise ValueError(
583
- f"the connection object given by tuple element at index {cur_idx} in `other_conn` is not "
584
- f"single -> method only possible for single connections")
585
- cur_idx += 1
586
- other_conn = Connection.based_on(other_conn)
587
- else:
588
- raise TypeError("the object given by `other_conn` has to be from type `Connection` or has to be a tuple "
589
- "of `Connection` objects")
259
+ if not other_conn.is_single():
260
+ raise ValueError(
261
+ "the other connection object is not single -> method only possible for single connections")
590
262
 
591
263
  intersection = []
592
264
 
593
- def determine_for(pieces: List[Union[Connection, Tuple[Connection]]], in_other_cnn: Connection):
594
- for cur_piece in pieces:
595
- if isinstance(cur_piece, Connection):
596
- if cur_piece.contained_in(in_other_cnn, ignore_metadata=True):
597
- intersection.append(cur_piece)
598
- else:
599
- # isinstance of tuple
600
- if Connection.check_if_tuple_contained_in_connection(cur_piece, in_other_cnn):
601
- intersection.append(cur_piece)
602
-
603
265
  #: check if some sub elements of self connection are contained in `other_conn`
604
- self_pieces = self.cut_into_all_possible_subtrees()
605
- determine_for(pieces=self_pieces, in_other_cnn=other_conn)
266
+ self_pieces = self.cut_into_all_possible_pieces()
267
+ intersection.extend(self.__class__.filter_connections_that_are_contained_in(
268
+ cnns_from=self_pieces,
269
+ are_contained_in=other_conn,
270
+ ignore_metadata=True
271
+ ))
606
272
 
607
273
  #: check if some sub elements of `other_conn` are contained in self connection
608
- other_pieces = other_conn.cut_into_all_possible_subtrees()
609
- determine_for(pieces=other_pieces, in_other_cnn=self)
274
+ other_pieces = other_conn.cut_into_all_possible_pieces()
275
+ intersection.extend(self.__class__.filter_connections_that_are_contained_in(
276
+ cnns_from=other_pieces,
277
+ are_contained_in=self,
278
+ ignore_metadata=True
279
+ ))
610
280
 
611
281
  #: filter all duplicated (and contained in each other) connections
612
282
  intersection_without_duplicates = []
613
283
  for cur_conn in intersection:
614
- checkable_cur_conn = Connection.based_on(cur_conn) if isinstance(cur_conn, tuple) else cur_conn
615
284
  found_it = False
616
285
  for cur_existing_conn in intersection_without_duplicates:
617
- checkable_cur_existing_conn = Connection.based_on(cur_existing_conn) \
618
- if isinstance(cur_existing_conn, tuple) else cur_existing_conn
619
- if checkable_cur_conn.equal_with(checkable_cur_existing_conn, ignore_metadata=True):
286
+ if cur_conn.equal_with(cur_existing_conn, ignore_metadata=True):
620
287
  found_it = True
621
288
  break
622
289
  if not found_it:
623
290
  intersection_without_duplicates.append(cur_conn)
291
+
624
292
  intersection_filtered = []
625
293
  #: filter all *contained in each other* connections
626
294
  for cur_conn in intersection_without_duplicates:
627
- usable_cur_conn = Connection.based_on(cur_conn) if isinstance(cur_conn, tuple) else cur_conn
628
295
  is_contained_in_another = False
629
296
  for cur_validate_cnn in intersection_without_duplicates:
630
- cur_usable_validate_cnn = \
631
- Connection.based_on(cur_validate_cnn) if isinstance(cur_validate_cnn, tuple) else cur_validate_cnn
632
297
  if cur_validate_cnn == cur_conn:
633
298
  # skip the same element
634
299
  continue
635
- if usable_cur_conn.contained_in(cur_usable_validate_cnn, ignore_metadata=True):
300
+ if cur_conn.contained_in(cur_validate_cnn, ignore_metadata=True):
636
301
  is_contained_in_another = True
637
302
  break
638
303
  if not is_contained_in_another:
304
+ if isinstance(cur_conn, (AndConnectionRelation,OrConnectionRelation)):
305
+ cur_conn = Connection.based_on(cur_conn)
639
306
  intersection_filtered.append(cur_conn)
640
307
 
641
308
  # only return unique objects
642
309
  return intersection_filtered
643
310
 
644
- # ---------------------------------- METHODS -----------------------------------------------------------------------
645
-
646
- def set_devices(self, from_device: Type[Device], to_device: Type[Device]):
311
+ def clone_without_based_on_elements(self) -> Connection:
647
312
  """
648
- Method for setting the devices of the connection if this has not yet been done during instantiation
649
-
650
- .. note::
651
- Note that you can not change the devices after they were set (only possible internally)
313
+ This method returns a copied version of this element, while all `_based_on_connections` are removed (the copied
314
+ element has an empty list here).
652
315
 
653
- :param from_device: device from which the connection starts
316
+ :return: a python copied object of this item
317
+ """
318
+ self_copy = copy.copy(self)
319
+ self_copy._based_on_connections = OrConnectionRelation() # pylint: disable=protected-access
320
+ return self_copy
654
321
 
655
- :param to_device: device at which the connection ends
322
+ def clone(self) -> Connection:
323
+ """
324
+ This method returns an exact clone of this connection. For this clone every inner connection object will be
325
+ newly instantiated, but all internal references (like the devices and so on) will not be copied (objects are the
326
+ same for this object and the clone). The method will make a normal copy for every connection object in the
327
+ `_based_on_elements` list.
656
328
  """
657
- if self.from_device is not None or self.to_device is not None:
658
- raise ValueError("devices already set")
659
- self._metadata["from_device"] = from_device
660
- self._metadata["to_device"] = to_device
329
+ self_copy = self.clone_without_based_on_elements()
330
+ self_copy.append_to_based_on(self._based_on_connections.clone()) # pylint: disable=protected-access
331
+ return self_copy
661
332
 
662
- def update_node_names(self, from_device_node_name: str, to_device_node_name: str) -> None:
333
+ def cut_into_all_possible_subtree_branches(self) -> List[Connection]:
334
+ """
335
+ This method returns a list of all possible connection tree branches. A branch is a single connection, while
336
+ this method returns a list of all possible singles where every single connection has this connection as head.
663
337
  """
664
- This method dates the names of the nodes from which the connection in the `from_device` originates and finally
665
- arrives in the` to_device`. Please provide the node name of your ``from_device`` in ``from_device_node_name``
666
- and the node name of your ``to_device`` in ``to_device_node_name``.
338
+ all_pieces = [self.clone()]
339
+ if len(self.based_on_elements) == 0:
340
+ return all_pieces
667
341
 
668
- :param from_device_node_name: specifies the node in the ``from_device``
342
+ if not self.is_single():
343
+ raise ValueError("the current connection object is not single -> method only possible for single "
344
+ "connections")
669
345
 
670
- :param to_device_node_name: specifies the node in the ``to_device``
671
- """
672
- self._metadata["from_device_node_name"] = from_device_node_name
673
- self._metadata["to_device_node_name"] = to_device_node_name
346
+ # return all possibilities of the relation while remain the current object as head
347
+ for sub_branch in self.based_on_elements.cut_into_all_possible_subtree_branches():
348
+ copied_conn = self.clone_without_based_on_elements()
349
+ copied_conn.append_to_based_on(sub_branch)
350
+ all_pieces.append(copied_conn)
351
+ return all_pieces
674
352
 
675
- def cut_into_all_possible_subtrees(self) -> List[Union[Connection, Tuple[Connection]]]:
353
+ def cut_into_all_possible_pieces(self) -> List[Connection]:
676
354
  """
677
355
  This method cuts the resolved connection tree in all possible pieces by removing elements that change
678
356
  the existing tree - thereby the method returns a list with all possibilities (a copy of this object with all
@@ -684,7 +362,7 @@ class Connection:
684
362
  """
685
363
  if self.__class__ == Connection:
686
364
  # this is only a container, execute process for every item of this one
687
- child_elems = self.based_on_elements
365
+ child_elems = self.based_on_elements.connections
688
366
  else:
689
367
  child_elems = [self]
690
368
 
@@ -692,32 +370,31 @@ class Connection:
692
370
 
693
371
  for cur_item in child_elems:
694
372
 
695
- if isinstance(cur_item, Connection):
696
- if not cur_item.is_single():
697
- raise ValueError("one of the given element is not single -> method only works with single items")
698
- elif isinstance(cur_item, tuple):
699
- tuple_idx = 0
700
- for cur_tuple_elem in cur_item:
701
- if not cur_tuple_elem.is_single():
702
- raise ValueError(
703
- f"one of the given tuple element has a item at index {tuple_idx}, that is not single -> "
704
- f"method only works with single items or tuple of single items")
705
- tuple_idx += 1
706
-
707
- cur_element = [cur_item]
708
- while len(cur_element) > 0:
709
- if isinstance(cur_element[0], Connection):
710
- all_pieces += Connection.__cut_conn_from_only_parent_to_child(cur_element[0])
711
- elif isinstance(cur_element[0], tuple):
712
- all_pieces += Connection.__cut_tuple_from_only_parent_to_child(cur_element[0])
373
+ if not cur_item.is_single():
374
+ raise ValueError("one of the given element is not single -> method only works with single items")
375
+
376
+ cur_element = cur_item
377
+ while cur_element:
378
+
379
+ if isinstance(cur_element, AndConnectionRelation):
713
380
  # now we can not go deeper, because we need this current AND connection
381
+ all_pieces += [Connection.based_on(and_relation)
382
+ for and_relation in cur_element.cut_into_all_possible_subtree_branches()]
714
383
  break
715
- cur_element = cur_element[0].based_on_elements
384
+
385
+ all_pieces += cur_element.cut_into_all_possible_subtree_branches()
386
+
387
+ if len(cur_element.based_on_elements.connections) > 1:
388
+ # should never be fulfilled because we have a single connection
389
+ raise ValueError('unexpected size of inner connections')
390
+
391
+ cur_element = cur_element.based_on_elements.connections[0] \
392
+ if cur_element.based_on_elements.connections else None
716
393
 
717
394
  # filter all duplicates
718
395
  return list(set(all_pieces))
719
396
 
720
- def set_metadata_for_all_subitems(self, metadata: Union[Dict[str, Union[Device, str]], None]):
397
+ def set_metadata_for_all_subitems(self, metadata: Union[ConnectionMetadata, None]):
721
398
  """
722
399
  This method sets the metadata for all existing Connection items in this element.
723
400
 
@@ -725,19 +402,14 @@ class Connection:
725
402
  metadata from every item)
726
403
  """
727
404
  if metadata is None:
728
- metadata = {
729
- "from_device": None, "to_device": None, "from_device_node_name": None, "to_device_node_name": None}
405
+ metadata = ConnectionMetadata()
406
+
407
+ if not isinstance(metadata, ConnectionMetadata):
408
+ raise TypeError('metadata must be an instance of `ConnectionMetadata`')
730
409
 
731
410
  for cur_base_elem in self._based_on_connections:
732
- if isinstance(cur_base_elem, Connection):
733
- cur_base_elem.set_metadata_for_all_subitems(metadata=metadata)
734
- elif isinstance(cur_base_elem, tuple):
735
- for cur_tuple_elem in cur_base_elem:
736
- cur_tuple_elem.set_metadata_for_all_subitems(metadata=metadata)
737
- else:
738
- raise TypeError(
739
- f"found illegal type {cur_base_elem.__class__} for based_on item at index {cur_base_elem}")
740
- self.metadata = metadata
411
+ cur_base_elem.set_metadata_for_all_subitems(metadata=metadata)
412
+ self._metadata = metadata
741
413
 
742
414
  def get_tree_str(self) -> str:
743
415
  """
@@ -756,15 +428,15 @@ class Connection:
756
428
  else:
757
429
  based_on_strings.append(cur_elem.get_tree_str())
758
430
 
759
- return f"{self.__class__.__name__}.based_on({', '.join(based_on_strings)})"
431
+ return f"{self.__class__.__name__}.based_on({'| '.join(based_on_strings)})"
760
432
 
761
- def is_bidirectional(self):
433
+ def is_bidirectional(self) -> Union[bool, None]:
762
434
  """
763
435
  Provides the information in which direction the connection is supported (if the property returns true, the
764
436
  connection must work in both directions, otherwise it is a unidirectional connection from the `from_device` to
765
437
  the` to_device`
766
438
  """
767
- return self._bidirectional
439
+ return self._metadata.bidirectional if self._metadata else None
768
440
 
769
441
  def is_universal(self):
770
442
  """
@@ -779,36 +451,27 @@ class Connection:
779
451
  Connection object of the tree - there are no undefined connection layers between an object and the given
780
452
  parent).
781
453
  """
782
- for cur_based_on in self.based_on_elements:
783
- if isinstance(cur_based_on, tuple):
784
- for cur_tuple_elem in cur_based_on:
785
- # check the next parent class type only if the main self type is not a container
786
- # (base `balder.Connection` object)
787
- if self.__class__ != Connection:
788
- if cur_tuple_elem not in self.__class__.get_parents():
789
- # only one element is not directly parent -> return false
790
- return False
791
- if not cur_tuple_elem.is_resolved():
792
- # only one subelement is not resolved -> return false
793
- return False
794
- else:
795
- # no tuple, single element
454
+ if self.__class__ == Connection:
455
+ return self.based_on_elements.is_resolved()
796
456
 
797
- # check the next parent class type only if the main self type is not a container
798
- # (base `balder.Connection` object)
799
- if self.__class__ != Connection:
800
- if cur_based_on.__class__ not in self.__class__.get_parents():
801
- # only one element is not directly parent -> return false
802
- return False
803
- if not cur_based_on.is_resolved():
804
- # only one subelement is not resolved -> return false
457
+ self_parents = self.__class__.get_parents()
458
+
459
+ for cur_based_on in self.based_on_elements.connections:
460
+ if isinstance(cur_based_on, Connection):
461
+ if cur_based_on.__class__ not in self_parents:
805
462
  return False
463
+ elif isinstance(cur_based_on, (AndConnectionRelation, OrConnectionRelation)):
464
+ for cur_type in cur_based_on.get_all_used_connection_types():
465
+ if cur_type not in self_parents:
466
+ return False
467
+ if not cur_based_on.is_resolved():
468
+ return False
806
469
  return True
807
470
 
808
471
  def is_single(self):
809
472
  """
810
- This method returns true if there exists no logical **OR** in the based on connection(s). One **AND** tuple is
811
- allowed.
473
+ This method returns true if there exists no logical **OR** in the based on connection(s). One **AND** relation
474
+ is allowed.
812
475
 
813
476
  .. note::
814
477
  Note that this method also returns False, if the connection is not completely resolved!
@@ -824,14 +487,6 @@ class Connection:
824
487
  if not self.is_resolved():
825
488
  return False
826
489
 
827
- if isinstance(self.based_on_elements[0], tuple):
828
- for cur_tuple_elem in self.based_on_elements[0]:
829
- # if only one element of tuple is not single -> return False
830
- if not cur_tuple_elem.is_single():
831
- return False
832
- # all elements are single
833
- return True
834
-
835
490
  return self.based_on_elements[0].is_single()
836
491
 
837
492
  def get_resolved(self) -> Connection:
@@ -842,60 +497,62 @@ class Connection:
842
497
 
843
498
  .. note::
844
499
  This method returns the connection without a container :class:`Connection`, if the container
845
- :class:`Connection` would only consist of one single connection (which is no tuple!). In this case the
846
- method returns this child connection directly without any :class:`Connection` container otherwise the
500
+ :class:`Connection` would only consist of one single connection (which is no AND relation!). In that case
501
+ the method returns this child connection directly without any :class:`Connection` container otherwise the
847
502
  :class:`Connection` container class with all resolved child classes will be returned.
848
503
  """
504
+ copied_base = self.clone_without_based_on_elements()
505
+
849
506
  if self.is_resolved():
850
- copied_base = self.clone()
507
+ copied_base.append_to_based_on(self.based_on_elements.get_simplified_relation())
508
+ elif self.__class__ == Connection:
509
+ # the base object is a container Connection - iterate over the items and determine the values for them
510
+ copied_base.append_to_based_on(self.based_on_elements.get_simplified_relation().get_resolved())
851
511
  else:
852
- copied_base = self.clone_without_based_on_elements()
853
-
854
- if self.__class__ == Connection:
855
- # the base object is a container Connection - iterate over the items and determine the values for them
856
- for cur_item in self.based_on_elements.copy():
857
- if isinstance(cur_item, tuple):
858
- new_tuple = tuple([cur_tuple_item.get_resolved() for cur_tuple_item in cur_item])
859
- copied_base.append_to_based_on(new_tuple)
860
- else:
861
- copied_base.append_to_based_on(cur_item.get_resolved())
862
- else:
863
- for next_higher_parent in self.based_on_elements:
864
- if isinstance(next_higher_parent, tuple):
865
- # determine all possibilities
866
- direct_ancestors_tuple = ()
867
- for cur_tuple_elem in next_higher_parent:
868
- if cur_tuple_elem.__class__ in self.__class__.get_parents():
869
- # element already is a direct ancestor
870
- direct_ancestors_tuple += (cur_tuple_elem, )
871
- else:
872
- all_pos_possibilities = []
873
- # add all possible direct parents to the possibilities list
874
- for cur_direct_parent in self.__class__.get_parents():
875
- if cur_direct_parent.is_parent_of(cur_tuple_elem.__class__):
876
- all_pos_possibilities.append(cur_direct_parent.based_on(cur_tuple_elem))
877
- direct_ancestors_tuple += (all_pos_possibilities, )
878
- # resolve the opportunities and create multiple possible tuples where all elements are direct
879
- # parents
880
- for cur_possibility in itertools.product(*direct_ancestors_tuple):
881
- new_child_tuple = (
882
- tuple([cur_tuple_element.get_resolved() for cur_tuple_element in cur_possibility]))
883
- copied_base.append_to_based_on(new_child_tuple)
884
- else:
885
- if next_higher_parent.__class__ in self.__class__.get_parents():
886
- # is already a direct parent
887
- copied_base.append_to_based_on(next_higher_parent.get_resolved())
512
+ # independent which based-on elements we have, we need to determine all elements between this connection
513
+ # and the elements of the relation
514
+ simplified_based_on = self.based_on_elements.get_simplified_relation()
515
+
516
+ for next_higher_parent in simplified_based_on:
517
+ if isinstance(next_higher_parent, AndConnectionRelation):
518
+ # determine all possibilities
519
+ direct_ancestors_relations = ()
520
+ for cur_and_elem in next_higher_parent:
521
+ # `cur_and_elem` needs to be a connection, because we are using simplified which has only
522
+ # `OR[AND[Cnn, ...], Cnn, ..]`
523
+ if cur_and_elem.__class__ in self.__class__.get_parents():
524
+ # element already is a direct ancestor
525
+ direct_ancestors_relations += (cur_and_elem, )
888
526
  else:
889
- # only add the first level of direct parents - deeper will be added by recursively call of
890
- # `get_resolved`
891
- for cur_self_direct_parent in self.__class__.get_parents():
892
- if next_higher_parent.__class__.is_parent_of(cur_self_direct_parent):
893
- new_child = cur_self_direct_parent.based_on(next_higher_parent)
894
- copied_base.append_to_based_on(new_child.get_resolved())
895
- # if it is a connection container, where only one element exists that is no tuple -> return this directly
527
+ all_pos_possibilities = []
528
+ # add all possible direct parents to the possibilities list
529
+ for cur_direct_parent in self.__class__.get_parents():
530
+ if cur_direct_parent.is_parent_of(cur_and_elem.__class__):
531
+ all_pos_possibilities.append(cur_direct_parent.based_on(cur_and_elem))
532
+ direct_ancestors_relations += (all_pos_possibilities, )
533
+ # resolve the opportunities and create multiple possible AND relations where all elements are
534
+ # direct parents
535
+ for cur_possibility in itertools.product(*direct_ancestors_relations):
536
+ new_child_relation = AndConnectionRelation(*cur_possibility).get_resolved()
537
+ copied_base.append_to_based_on(new_child_relation)
538
+ else:
539
+ # `next_higher_parent` needs to be a connection, because we are using simplified which has only
540
+ # `OR[AND[Cnn, ...], Cnn, ..]`
541
+ if next_higher_parent.__class__ in self.__class__.get_parents():
542
+ # is already a direct parent
543
+ copied_base.append_to_based_on(next_higher_parent.get_resolved())
544
+ continue
545
+ # only add the first level of direct parents - deeper will be added by recursively call of
546
+ # `get_resolved`
547
+ for cur_self_direct_parent in self.__class__.get_parents():
548
+ if next_higher_parent.__class__.is_parent_of(cur_self_direct_parent):
549
+ new_child = cur_self_direct_parent.based_on(next_higher_parent)
550
+ copied_base.append_to_based_on(new_child.get_resolved())
551
+
552
+ # if it is a connection container, where only one element exists that is no AND relation -> return this directly
896
553
  # instead of the container
897
554
  if copied_base.__class__ == Connection and len(copied_base.based_on_elements) == 1 and not \
898
- isinstance(copied_base.based_on_elements[0], tuple):
555
+ isinstance(copied_base.based_on_elements[0], AndConnectionRelation):
899
556
  return copied_base.based_on_elements[0]
900
557
 
901
558
  return copied_base
@@ -910,42 +567,29 @@ class Connection:
910
567
  If the current object is a container :class:`Connection` object (direct instance of :class:`Connection`),
911
568
  this method returns the single elements without a container class!
912
569
  """
913
- all_singles = []
570
+ # note: This method always work with the resolved and simplified version of the object because it is using
571
+ # `get_resolved()`.
572
+ if self.is_universal():
573
+ return [self]
914
574
 
915
- all_self_items = [self]
916
- if self.__class__ == Connection and len(self.based_on_elements) == 0:
575
+ if self.is_single():
917
576
  return [self]
918
- if self.__class__ == Connection:
919
- all_self_items = self.based_on_elements
920
- for cur_item in all_self_items:
921
- if isinstance(cur_item, tuple):
922
- all_singles += Connection.convert_tuple_to_singles(cur_item)
923
- elif cur_item.is_single():
924
- all_singles.append(copy.copy(cur_item))
925
- else:
926
- # do this for resolved version of this object
927
- resolved_obj = cur_item.get_resolved()
928
-
929
- if len(resolved_obj.based_on_elements) == 0:
930
- # element has no children -> return only this
931
- return [resolved_obj]
932
- for cur_child in resolved_obj.based_on_elements:
933
- for cur_single_child in cur_child.get_singles():
934
- copied_base = resolved_obj.clone_without_based_on_elements()
935
- copied_base.append_to_based_on(cur_single_child)
936
- all_singles.append(copied_base)
937
- # convert all tuple objects in a connection object and set metadata of self object
938
- cleaned_singles = []
939
-
940
- for cur_single in all_singles:
941
- if isinstance(cur_single, Connection):
942
- new_cnn = cur_single
943
- else:
944
- new_cnn = Connection.based_on(cur_single)
945
577
 
946
- new_cnn.set_metadata_for_all_subitems(self.metadata)
947
- cleaned_singles.append(new_cnn)
948
- return cleaned_singles
578
+ all_singles = []
579
+
580
+ resolved_self = self.get_resolved()
581
+
582
+ for cur_child in resolved_self.based_on_elements:
583
+ for cur_single_child in cur_child.get_singles():
584
+ cur_single_child = cur_single_child.clone()
585
+ if self.__class__ == Connection and isinstance(cur_single_child, Connection):
586
+ all_singles.append(cur_single_child)
587
+ else:
588
+ copied_base = resolved_self.clone_without_based_on_elements()
589
+ copied_base.append_to_based_on(cur_single_child)
590
+ all_singles.append(copied_base)
591
+
592
+ return all_singles
949
593
 
950
594
  def get_conn_partner_of(self, device: Type[Device], node: Union[str, None] = None) -> Tuple[Type[Device], str]:
951
595
  """
@@ -956,30 +600,15 @@ class Connection:
956
600
  :param node: the node name of the device itself (only required if the connection starts and ends with the same
957
601
  device)
958
602
  """
959
- if device not in (self.from_device, self.to_device):
960
- raise ValueError(f"the given device `{device.__qualname__}` is no component of this connection")
961
- if node is None:
962
- # check that the from_device and to_device are not the same
963
- if self.from_device == self.to_device:
964
- raise ValueError("the connection is a inner-device connection (start and end is the same device) - you "
965
- "have to provide the `node` string too")
966
- if device == self.from_device:
967
- return self.to_device, self.to_node_name
968
-
969
- return self.from_device, self.from_node_name
970
-
971
- if node not in (self.from_node_name, self.to_node_name):
972
- raise ValueError(f"the given node `{node}` is no component of this connection")
973
-
974
- if device == self.from_device and node == self.from_node_name:
975
- return self.to_device, self.to_node_name
976
-
977
- if device == self.to_device and node == self.to_node_name:
978
- return self.from_device, self.from_node_name
979
-
980
- raise ValueError(f"the given node `{node}` is no component of the given device `{device.__qualname__}`")
603
+ return self.metadata.get_conn_partner_of(device, node)
981
604
 
982
- def has_connection_from_to(self, start_device, end_device=None):
605
+ def has_connection_from_to(
606
+ self,
607
+ start_device: Type[Device],
608
+ start_device_node_name: Union[str, None] = None,
609
+ end_device: Union[Type[Device], None] = None,
610
+ end_device_node_name: Union[str, None] = None,
611
+ ) -> bool:
983
612
  """
984
613
  This method checks if there is a connection from ``start_device`` to ``end_device``. This will return
985
614
  true if the ``start_device`` and ``end_device`` given in this method are also the ``start_device`` and
@@ -989,25 +618,20 @@ class Connection:
989
618
 
990
619
  :param start_device: the device for which the method should check whether it is a communication partner (for
991
620
  non-bidirectional connection, this has to be the start device)
992
-
621
+ :param start_device_node_name: the node name that start device should have or None if it should be ignored
993
622
  :param end_device: the other device for which the method should check whether it is a communication partner (for
994
623
  non-bidirectional connection, this has to be the end device - this is optional if only the
995
624
  start device should be checked)
625
+ :param end_device_node_name: the node name that start device should have or None if it should be ignored
996
626
 
997
627
  :return: returns true if the given direction is possible
998
628
  """
999
- if end_device is None:
1000
-
1001
- if self.is_bidirectional():
1002
- return start_device in (self.from_device, self.to_device)
1003
-
1004
- return start_device == self.from_device
1005
-
1006
- if self.is_bidirectional():
1007
- return start_device == self.from_device and end_device == self.to_device or \
1008
- start_device == self.to_device and end_device == self.from_device
1009
-
1010
- return start_device == self.from_device and end_device == self.to_device
629
+ return self.metadata.has_connection_from_to(
630
+ start_device=start_device,
631
+ start_device_node_name=start_device_node_name,
632
+ end_device=end_device,
633
+ end_device_node_name=end_device_node_name
634
+ )
1011
635
 
1012
636
  def equal_with(self, other_conn: Connection, ignore_metadata=False) -> bool:
1013
637
  """
@@ -1020,11 +644,7 @@ class Connection:
1020
644
 
1021
645
  .. note::
1022
646
  Note that it doesn't matter if the connection is embedded in a container-Connection element (direct
1023
- instance of :class`Connection`) or not. It only checks, that the logical data of them are the same. If both
1024
- elements are a container for a list of child connections, the method secures that both has the same
1025
- children. If only one (same for both) has one child connection which is embedded in a container
1026
- :class:`Connection` class, it returns also true if the other connection is like the one child element of
1027
- the other container :class:`Connection`.
647
+ instance of :class`Connection`) or not. It only checks, that the logical data of them are the same.
1028
648
 
1029
649
  :param other_conn: the other connection, this connection will be compared with
1030
650
 
@@ -1032,11 +652,8 @@ class Connection:
1032
652
 
1033
653
  :return: returns True if both elements are same
1034
654
  """
1035
- if self.__class__ not in (Connection, other_conn.__class__):
1036
- return False
1037
-
1038
655
  if not ignore_metadata:
1039
- metadata_check_result = self._metadata_equal_with(of_connection=other_conn)
656
+ metadata_check_result = self.metadata.equal_with(other_conn.metadata)
1040
657
  if metadata_check_result is False:
1041
658
  return False
1042
659
 
@@ -1045,37 +662,18 @@ class Connection:
1045
662
  resolved_self = self.get_resolved()
1046
663
  resolved_other = other_conn.get_resolved()
1047
664
 
1048
- self_based_on_elems = [
1049
- cur_elem for cur_elem in resolved_self.based_on_elements if isinstance(cur_elem, Connection)]
1050
- self_based_on_tuples = [
1051
- cur_elem for cur_elem in resolved_self.based_on_elements if isinstance(cur_elem, tuple)]
1052
- other_based_on_elems = [
1053
- cur_elem for cur_elem in resolved_other.based_on_elements if isinstance(cur_elem, Connection)]
1054
- other_based_on_tuples = [
1055
- cur_elem for cur_elem in resolved_other.based_on_elements if isinstance(cur_elem, tuple)]
1056
-
1057
- # check single connection elements (if they match all in both directions)
1058
- if (not self.__class__.check_equal_connections_are_in(
1059
- cnns_from=self_based_on_elems, are_in_cnns_from=other_based_on_elems, ignore_metadata=ignore_metadata)
1060
- or not self.__class__.check_equal_connections_are_in(
1061
- cnns_from=other_based_on_elems, are_in_cnns_from=self_based_on_elems,
1062
- ignore_metadata=ignore_metadata)):
1063
- return False
1064
-
1065
- # check tuple connection elements (if they match all in both directions)
1066
- if (not self.__class__.check_equal_connection_tuples_are_in(
1067
- tuples_from=self_based_on_tuples, are_in_tuples_from=other_based_on_tuples,
1068
- ignore_metadata=ignore_metadata)
1069
- or not self.__class__.check_equal_connection_tuples_are_in(
1070
- tuples_from=other_based_on_tuples, are_in_tuples_from=self_based_on_tuples,
1071
- ignore_metadata=ignore_metadata)):
665
+ if self.__class__ != other_conn.__class__:
1072
666
  return False
1073
667
 
1074
- return True
668
+ return resolved_self.based_on_elements.equal_with(resolved_other.based_on_elements)
1075
669
 
1076
- def contained_in(self, other_conn: Connection, ignore_metadata=False) -> bool:
670
+ def contained_in(
671
+ self,
672
+ other_conn: Connection | AndConnectionRelation | OrConnectionRelation,
673
+ ignore_metadata=False
674
+ ) -> bool:
1077
675
  """
1078
- This method helps to find out whether this connection-tree fits within a given connection tree. A connection
676
+ This method helps to find out whether this connection-tree fits within another connection tree. A connection
1079
677
  object is a certain part of the large connection tree that Balder has at its disposal. This method checks
1080
678
  whether a possibility of this connection tree fits in one possibility of the given connection tree.
1081
679
 
@@ -1089,8 +687,11 @@ class Connection:
1089
687
 
1090
688
  :return: true if the self object is contained in the `other_conn`, otherwise false
1091
689
  """
690
+ # note: This method always work with the resolved and simplified version of the object because it is using
691
+ # `get_resolved()`.
692
+
1092
693
  if not ignore_metadata:
1093
- metadata_check_result = self._metadata_contained_in(of_connection=other_conn)
694
+ metadata_check_result = self.metadata.contained_in(other_conn.metadata)
1094
695
  if metadata_check_result is False:
1095
696
  return False
1096
697
 
@@ -1102,156 +703,103 @@ class Connection:
1102
703
  # one of the resolved object is a raw `Connection` object without based-on-elements -> always true
1103
704
  return True
1104
705
 
1105
- self_possibilities = [resolved_self]
1106
706
  if resolved_self.__class__ == Connection:
1107
- self_possibilities = resolved_self.based_on_elements
1108
- for cur_self in self_possibilities:
1109
- if isinstance(cur_self, tuple):
1110
- if Connection.check_if_tuple_contained_in_connection(cur_self, resolved_other):
707
+ return resolved_self.based_on_elements.contained_in(resolved_other, ignore_metadata=ignore_metadata)
708
+
709
+ if resolved_self.__class__ == resolved_other.__class__:
710
+ # The element itself has already matched, now we still have to check whether at least one inner element
711
+ # of this type is contained in the minimum one element of the other
712
+ singles_self = resolved_self.get_singles()
713
+ singles_other = resolved_other.get_singles()
714
+
715
+ # check that for one single_self element all hierarchical based_on elements are in one of the single
716
+ # other element
717
+ for cur_single_self, cur_single_other in itertools.product(singles_self, singles_other):
718
+ # check if both consists of only one element
719
+ if len(cur_single_self.based_on_elements) == 0:
720
+ # the cur self single is only one element -> this is contained in the other
1111
721
  return True
1112
- elif not cur_self.__class__ == resolved_other.__class__:
1113
- for cur_based_on_elem in resolved_other.based_on_elements:
1114
- if isinstance(cur_based_on_elem, tuple):
1115
- # check if the current connection fits in one of the tuple items -> allowed too (like a smaller
1116
- # AND contained in a bigger AND)
1117
- for cur_other_tuple_element in cur_based_on_elem:
1118
- if cur_self.contained_in(cur_other_tuple_element, ignore_metadata=ignore_metadata):
1119
- return True
1120
- else:
1121
- if cur_self.contained_in(cur_based_on_elem, ignore_metadata=ignore_metadata):
1122
- # element was found in this branch
1123
- return True
1124
- else:
1125
- # The element itself has already matched, now we still have to check whether at least one tuple or a
1126
- # connection (i.e. one element of the list) is matched with one of the other
1127
- singles_self = cur_self.get_singles()
1128
- singles_other = resolved_other.get_singles()
1129
-
1130
- # check that for one single_self element all hierarchical based_on elements are in one of the single
1131
- # other element
1132
- for cur_single_self in singles_self:
1133
- for cur_single_other in singles_other:
1134
- # check if both consists of only one element
1135
- if len(cur_single_self.based_on_elements) == 0:
1136
- # the cur self single is only one element -> this is contained in the other
1137
- return True
1138
722
 
1139
- if len(cur_single_other.based_on_elements) == 0:
1140
- # the other element is only one element, but the self element not -> contained_in
1141
- # for this single definitely false
1142
- continue
1143
-
1144
- # note: for both only one `based_on_elements` is possible, because they are singles
1145
- if isinstance(cur_single_self.based_on_elements[0], tuple) and \
1146
- isinstance(cur_single_other.based_on_elements[0], tuple):
1147
- # both are tuples -> check that there exist a matching where all are contained_in
1148
- tuple_is_complete_contained_in = True
1149
- # check that every tuple element of self is contained in minimum one tuple elements of each
1150
- # other
1151
- for cur_self_tuple_element in cur_single_self.based_on_elements[0]:
1152
- find_some_match_for_cur_self_tuple_element = False
1153
- for cur_other_tuple_element in cur_single_other.based_on_elements[0]:
1154
- if cur_self_tuple_element.contained_in(
1155
- cur_other_tuple_element, ignore_metadata=ignore_metadata):
1156
- # find a match, where the current tuple element is contained in one tuple
1157
- # element of the other
1158
- find_some_match_for_cur_self_tuple_element = True
1159
- break
1160
- if not find_some_match_for_cur_self_tuple_element:
1161
- tuple_is_complete_contained_in = False
1162
- break
1163
- if tuple_is_complete_contained_in:
1164
- # find a complete valid match
1165
- return True
1166
- elif isinstance(cur_single_self.based_on_elements[0], Connection) and \
1167
- isinstance(cur_single_other.based_on_elements[0], Connection):
1168
- # both are connection trees -> check if the subtrees are contained in
1169
- if cur_single_self.based_on_elements[0].contained_in(
1170
- cur_single_other.based_on_elements[0], ignore_metadata=ignore_metadata):
1171
- # find a complete valid match
1172
- return True
1173
- elif isinstance(cur_single_self.based_on_elements[0], tuple) and \
1174
- isinstance(cur_single_other.based_on_elements[0], Connection):
1175
- # this is allowed too, if every tuple item is contained_in the relevant connection
1176
- # check that every tuple element of self is contained in the other connection
1177
- for cur_self_tuple_element in cur_single_self.based_on_elements[0]:
1178
- if not cur_self_tuple_element.contained_in(
1179
- cur_single_other, ignore_metadata=ignore_metadata):
1180
- # find a match, where the current tuple element is not contained in the other
1181
- # connection -> tuple can not be contained in
1182
- return False
723
+ if len(cur_single_other.based_on_elements) == 0:
724
+ # the other element is only one element, but the self element not -> contained_in
725
+ # for this single definitely false
726
+ continue
727
+
728
+ # note: for both only one `based_on_elements` is possible, because they are singles
729
+ self_first_based_on = cur_single_self.based_on_elements[0]
730
+ other_first_based_on = cur_single_other.based_on_elements[0]
731
+
732
+ self_is_and = isinstance(self_first_based_on, AndConnectionRelation)
733
+ self_is_cnn = isinstance(self_first_based_on, Connection)
734
+ other_is_and = isinstance(other_first_based_on, AndConnectionRelation)
735
+ other_is_cnn = isinstance(other_first_based_on, Connection)
736
+
737
+ if self_is_and and (other_is_and or other_is_cnn) or self_is_cnn and other_is_cnn:
738
+ # find a complete valid match
739
+ if self_first_based_on.contained_in(other_first_based_on, ignore_metadata=ignore_metadata):
740
+ return True
741
+ # skip all others possibilities
742
+ else:
743
+ # the elements itself do not match -> go deeper within the other connection
744
+ if isinstance(resolved_other, AndConnectionRelation):
745
+ # check if the current connection fits in one of the AND relation items -> allowed too (f.e. a
746
+ # smaller AND contained in a bigger AND)
747
+ for cur_other_and_element in resolved_other.connections:
748
+ if resolved_self.contained_in(cur_other_and_element, ignore_metadata=ignore_metadata):
749
+ return True
750
+
751
+ resolved_other_relation = resolved_other.based_on_elements \
752
+ if isinstance(resolved_other, Connection) else resolved_other
753
+
754
+ for cur_other_based_on in resolved_other_relation.connections:
755
+ if isinstance(cur_other_based_on, AndConnectionRelation):
756
+ # check if the current connection fits in one of the AND relation items -> allowed too (f.e. a
757
+ # smaller AND contained in a bigger AND)
758
+ for cur_other_and_element in cur_other_based_on.connections:
759
+ if resolved_self.contained_in(cur_other_and_element, ignore_metadata=ignore_metadata):
1183
760
  return True
1184
- # skip all others possibilities
761
+ else:
762
+ if resolved_self.contained_in(cur_other_based_on, ignore_metadata=ignore_metadata):
763
+ # element was found in this branch
764
+ return True
1185
765
  return False
1186
766
 
1187
767
  def intersection_with(
1188
- self, other_conn: Union[Connection, Type[Connection],
1189
- List[Connection, Type[Connection], Tuple[Connection]]]) \
768
+ self, other_conn: Union[Connection, Type[Connection], AndConnectionRelation, OrConnectionRelation]) \
1190
769
  -> Union[Connection, None]:
1191
770
  """
1192
771
  This method returns a list of subtrees that describe the intersection of this connection subtree and the
1193
772
  given ones. Note that this method converts all connections in **single** **resolved** connections first.
1194
- For these connections the method checks if there are common intersections between the elements of this object
1195
- and the given connection elements.
773
+ The method checks if there are common intersections between the elements of this object and the given connection
774
+ elements within the single connections.
1196
775
  The method cleans up this list and only return unique sub-connection trees!
1197
776
 
1198
777
  :param other_conn: the other sub connection tree list
1199
778
 
1200
779
  :return: the intersection connection or none, if the method has no intersection
1201
780
  """
1202
- if isinstance(other_conn, type):
1203
- if not issubclass(other_conn, Connection):
1204
- raise TypeError("the given `other_conn` has to be from type `Connection`")
1205
- other_conn = other_conn()
1206
-
1207
- if isinstance(other_conn, Connection):
1208
- if other_conn.__class__ == Connection:
1209
- if len(other_conn.based_on_elements) == 0:
1210
- return self.clone()
1211
- other_conn = other_conn.based_on_elements
1212
- else:
1213
- other_conn = [other_conn]
781
+ other_conn = cnn_type_check_and_convert(other_conn)
782
+
783
+ if isinstance(other_conn, Connection) and other_conn.__class__ == Connection:
784
+ if len(other_conn.based_on_elements) == 0:
785
+ return self.clone()
786
+ other_conn = other_conn.based_on_elements
1214
787
 
1215
- if self.__class__ == Connection and len(self.based_on_elements) == 0:
1216
- return Connection.based_on(*other_conn).clone()
788
+ if self.is_universal():
789
+ return other_conn.clone() if isinstance(other_conn, Connection) else Connection.based_on(other_conn.clone())
1217
790
 
1218
- # determine all single connection of the two sides (could contain tuple, where every element is a single
791
+ # determine all single connection of the two sides (could contain AND relations, where every element is a single
1219
792
  # connection too)
1220
- self_conn_singles = []
1221
- other_conn_singles = []
1222
-
1223
- idx = 0
1224
- for cur_self_elem in self.get_singles():
1225
- if isinstance(cur_self_elem, Connection):
1226
- self_conn_singles.append(cur_self_elem)
1227
- elif isinstance(cur_self_elem, type) and issubclass(cur_self_elem, Connection):
1228
- self_conn_singles.append(cur_self_elem())
1229
- else:
1230
- raise TypeError(f"the element at index {idx} of `self_conn` is not from type `Connection` or is a "
1231
- f"tuple of that")
1232
-
1233
- idx = 0
1234
- for cur_other_elem in other_conn:
1235
- if isinstance(cur_other_elem, tuple):
1236
- other_conn_singles += Connection.convert_tuple_to_singles(cur_other_elem)
1237
- elif isinstance(cur_other_elem, Connection):
1238
- other_conn_singles += cur_other_elem.get_singles()
1239
- elif isinstance(cur_other_elem, type) and issubclass(cur_other_elem, Connection):
1240
- other_conn_singles.append(cur_other_elem())
1241
- else:
1242
- raise TypeError(f"the element at index {idx} of `other_conn` is not from type `Connection` or is a "
1243
- f"tuple of that")
1244
- idx += 1
793
+ self_conn_singles = self.get_singles()
794
+ other_conn_singles = other_conn.get_singles()
1245
795
 
1246
796
  intersections = []
1247
797
  # determine intersections between all of these single components
1248
- for cur_self_conn in self_conn_singles:
1249
- for cur_other_conn in other_conn_singles:
1250
- for cur_intersection in cur_self_conn.get_intersection_with_other_single(cur_other_conn):
1251
- if isinstance(cur_intersection, tuple):
1252
- cur_intersection = Connection.based_on(cur_intersection)
1253
- if cur_intersection not in intersections:
1254
- intersections.append(cur_intersection)
798
+ for cur_self_conn, cur_other_conn in itertools.product(self_conn_singles, other_conn_singles):
799
+ for cur_intersection in cur_self_conn.get_intersection_with_other_single(cur_other_conn):
800
+ intersections.append(cur_intersection)
801
+
802
+ intersections = set(intersections)
1255
803
 
1256
804
  #: filter all *contained in each other* connections
1257
805
  intersection_filtered = []
@@ -1270,79 +818,50 @@ class Connection:
1270
818
  if len(intersection_filtered) == 0:
1271
819
  # there is no intersection
1272
820
  return None
1273
- if len(intersection_filtered) > 1 or isinstance(intersection_filtered[0], tuple):
1274
- return Connection.based_on(*intersection_filtered).clone()
1275
821
 
1276
- return intersection_filtered[0].clone()
822
+ return Connection.based_on(OrConnectionRelation(*[cnn.clone() for cnn in intersection_filtered]))
1277
823
 
1278
824
  def append_to_based_on(
1279
- self, *args: Union[Connection, Type[Connection], Tuple[Union[Type[Connection], Connection]]]) -> None:
825
+ self, *args: Union[Type[Connection], Connection, OrConnectionRelation, AndConnectionRelation]) -> None:
1280
826
  """
1281
827
  with this method you can extend the internal based_on list with the transferred elements. Any number of
1282
- :meth:`Connection` objects or tuples with :meth:`Connection` objects can be given to this method
828
+ :meth:`Connection` objects or relations with :meth:`Connection` objects can be given to this method. They will
829
+ all be added to the internal OR relation.
1283
830
 
1284
831
  :param args: all connection items that should be added here
1285
832
  """
1286
833
 
1287
- for cur_idx, cur_connection in enumerate(args):
1288
- if isinstance(cur_connection, type):
1289
- if not issubclass(cur_connection, Connection):
1290
- raise TypeError(f"illegal type `{cur_connection.__name__}` for parameter number {cur_idx}")
1291
- if self.__class__ != Connection:
1292
- if not cur_connection.is_parent_of(self.__class__):
1293
- raise IllegalConnectionTypeError(
1294
- f"the given connection `{cur_connection.__name__}` (parameter pos {cur_idx}) is no parent "
1295
- f"class of the `{self.__class__.__name__}`")
1296
- # this is a simple Connection type object -> simply add an instance of it to the full list
1297
- new_conn = cur_connection()
1298
- self._based_on_connections.append(new_conn)
1299
-
1300
- elif isinstance(cur_connection, Connection):
1301
- if cur_connection.__class__ == Connection:
1302
- raise ValueError(f"it is not allowed to provide a container Connection object in based_on items - "
1303
- f"found at index {cur_idx}")
1304
- # `based_on` call for this sub-connection because we get an `Connection` object
1305
- if self.__class__ != Connection:
1306
- if not cur_connection.__class__.is_parent_of(self.__class__):
1307
- raise IllegalConnectionTypeError(
1308
- f"the given connection `{cur_connection.__class__.__name__}` (parameter pos {cur_idx}) is "
1309
- f"no parent class of the `{self.__class__.__name__}`")
1310
- self._based_on_connections.append(cur_connection)
1311
-
1312
- elif isinstance(cur_connection, tuple):
1313
- result_tuple = ()
1314
- for cur_tuple_idx, cur_tuple_elem in enumerate(cur_connection):
1315
- if isinstance(cur_tuple_elem, type):
1316
- if not issubclass(cur_tuple_elem, Connection):
1317
- raise TypeError(f"illegal type `{cur_tuple_elem.__name__}` for tuple element "
1318
- f"{cur_tuple_idx} for parameter number {cur_idx}")
1319
- if self.__class__ != Connection:
1320
- if not cur_tuple_elem.is_parent_of(self.__class__):
1321
- raise IllegalConnectionTypeError(
1322
- f"the given connection `{cur_tuple_elem.__name__}` (tuple element {cur_tuple_idx} "
1323
- f"for parameter at pos {cur_idx}) is no parent class of the "
1324
- f"`{self.__class__.__name__}`")
1325
- # this is a simple Connection type object -> simply add the instance of connection to result
1326
- # tuple
1327
- result_tuple += (cur_tuple_elem(), )
1328
- elif isinstance(cur_tuple_elem, Connection):
1329
- # `based_on` call for this sub-connection because we get an `Connection` object
1330
- if self.__class__ != Connection:
1331
- if not cur_tuple_elem.__class__.is_parent_of(self.__class__):
1332
- raise IllegalConnectionTypeError(
1333
- f"the given connection `{cur_tuple_elem.__class__.__name__}` (tuple element "
1334
- f"{cur_tuple_idx} for parameter at pos {cur_idx}) is no parent class of the "
1335
- f"`{self.__class__.__name__}`")
1336
- result_tuple += (cur_tuple_elem, )
1337
- elif isinstance(cur_tuple_elem, tuple):
1338
- raise TypeError(f"nested tuples (tuple element {cur_tuple_idx} for parameter at pos {cur_idx}) "
1339
- f"and thus nested AND operations are not possible")
1340
- elif isinstance(cur_tuple_elem, list):
1341
- raise TypeError(f"nested lists (tuple element {cur_tuple_idx} for parameter at pos {cur_idx}) "
1342
- f"and thus nested OR/AND operations are not possible")
1343
- else:
1344
- raise TypeError(f"illegal type `{cur_tuple_elem.__name__}` for tuple element {cur_tuple_idx} "
1345
- f"for parameter at pos {cur_idx}")
1346
- self._based_on_connections.append(result_tuple)
834
+ def validate_that_subconnection_is_parent(idx, connection_type: Type[Connection]):
835
+ if connection_type == Connection:
836
+ raise ValueError(f"it is not allowed to provide a container Connection object in based_on items - "
837
+ f"found at index {idx}")
838
+ # `based_on` call for this sub-connection because we get an `Connection` object
839
+ if self.__class__ != Connection:
840
+ if not connection_type.is_parent_of(self.__class__):
841
+ raise IllegalConnectionTypeError(
842
+ f"the connection `{cur_elem.__class__.__name__}` (at parameter pos {idx}) is "
843
+ f"no parent class of the `{self.__class__.__name__}`")
844
+
845
+ for cur_idx, cur_elem in enumerate(args):
846
+ if isinstance(cur_elem, type) and issubclass(cur_elem, Connection):
847
+ cur_elem = cur_elem()
848
+
849
+ if isinstance(cur_elem, Connection):
850
+ if cur_elem.is_universal():
851
+ # ignore it, because it is irrelevant for this connection
852
+ continue
853
+ validate_that_subconnection_is_parent(cur_idx, cur_elem.__class__)
854
+ self._based_on_connections.append(cur_elem)
855
+ elif isinstance(cur_elem, OrConnectionRelation):
856
+ for cur_inner_elem in cur_elem.connections:
857
+ if isinstance(cur_inner_elem, Connection) and cur_inner_elem.is_universal():
858
+ # ignore it, because it is irrelevant for this connection
859
+ continue
860
+ validate_that_subconnection_is_parent(cur_idx, cur_inner_elem.__class__)
861
+ self._based_on_connections.append(cur_inner_elem)
862
+ elif isinstance(cur_elem, AndConnectionRelation):
863
+ for cur_inner_type in cur_elem.get_all_used_connection_types():
864
+ validate_that_subconnection_is_parent(cur_idx, cur_inner_type)
865
+ self._based_on_connections.append(cur_elem)
1347
866
  else:
1348
- raise TypeError(f"illegal type `{cur_connection.__name__}` for parameter at pos {cur_idx}")
867
+ raise TypeError(f"illegal type `{cur_elem.__name__}` for parameter at pos {cur_idx}")